├── .gitignore ├── README.md ├── colorbleed ├── __init__.py ├── action.py ├── api.py ├── fusion │ ├── __init__.py │ └── lib.py ├── houdini │ ├── __init__.py │ └── lib.py ├── launcher_actions.py ├── lib.py ├── maya │ ├── __init__.py │ ├── action.py │ ├── customize.py │ ├── lib.py │ ├── menu.json │ ├── menu.py │ └── plugin.py ├── plugin.py ├── plugins │ ├── fusion │ │ ├── create │ │ │ └── create_tiff_saver.py │ │ ├── inventory │ │ │ ├── select_containers.py │ │ │ └── set_tool_color.py │ │ ├── load │ │ │ ├── actions.py │ │ │ └── load_sequence.py │ │ └── publish │ │ │ ├── collect_comp.py │ │ │ ├── collect_fusion_version.py │ │ │ ├── collect_instances.py │ │ │ ├── collect_render_target.py │ │ │ ├── increment_current_file_deadline.py │ │ │ ├── publish_image_sequences.py │ │ │ ├── render_local.py │ │ │ ├── save_scene.py │ │ │ ├── submit_deadline.py │ │ │ ├── validate_background_depth.py │ │ │ ├── validate_comp_saved.py │ │ │ ├── validate_create_folder_checked.py │ │ │ ├── validate_filename_has_extension.py │ │ │ ├── validate_saver_has_input.py │ │ │ ├── validate_saver_passthrough.py │ │ │ └── validate_unique_subsets.py │ ├── global │ │ ├── load │ │ │ ├── copy_file.py │ │ │ ├── copy_file_path.py │ │ │ └── open_imagesequence.py │ │ └── publish │ │ │ ├── cleanup.py │ │ │ ├── collect_assumed_destination.py │ │ │ ├── collect_comment.py │ │ │ ├── collect_context_label.py │ │ │ ├── collect_current_shell_file.py │ │ │ ├── collect_deadline_user.py │ │ │ ├── collect_filesequences.py │ │ │ ├── collect_machine_name.py │ │ │ ├── collect_project_code.py │ │ │ ├── collect_shell_workspace.py │ │ │ ├── collect_time.py │ │ │ ├── integrate.py │ │ │ ├── submit_publish_job.py │ │ │ ├── validate_file_saved.py │ │ │ ├── validate_resources.py │ │ │ └── validate_sequence_frames.py │ ├── houdini │ │ ├── create │ │ │ ├── create_alembic_camera.py │ │ │ ├── create_pointcache.py │ │ │ └── create_vbd_cache.py │ │ ├── load │ │ │ ├── actions.py │ │ │ ├── load_alembic.py │ │ │ ├── load_camera.py │ │ │ └── load_vdb.py │ │ └── publish │ │ │ ├── collect_current_file.py │ │ │ ├── collect_frames.py │ │ │ ├── collect_instances.py │ │ │ ├── collect_output_node.py │ │ │ ├── collect_workscene_fps.py │ │ │ ├── extract_alembic.py │ │ │ ├── extract_vdb_cache.py │ │ │ ├── validate_alembic_input_node.py │ │ │ ├── validate_bypass.py │ │ │ ├── validate_camera_rop.py │ │ │ ├── validate_file_extension.py │ │ │ ├── validate_frame_token.py │ │ │ ├── validate_mkpaths_toggled.py │ │ │ ├── validate_output_node.py │ │ │ ├── validate_primitive_hierarchy_paths.py │ │ │ └── validate_vdb_input_node.py │ └── maya │ │ ├── create │ │ ├── colorbleed_animation.py │ │ ├── colorbleed_camera.py │ │ ├── colorbleed_fbx.py │ │ ├── colorbleed_look.py │ │ ├── colorbleed_mayaascii.py │ │ ├── colorbleed_model.py │ │ ├── colorbleed_pointcache.py │ │ ├── colorbleed_renderglobals.py │ │ ├── colorbleed_rig.py │ │ ├── colorbleed_setdress.py │ │ ├── colorbleed_vrayproxy.py │ │ ├── colorbleed_vrayscene.py │ │ ├── colorbleed_yeti_cache.py │ │ └── colorbleed_yeti_rig.py │ │ ├── load │ │ ├── actions.py │ │ ├── load_alembic.py │ │ ├── load_camera.py │ │ ├── load_fbx.py │ │ ├── load_look.py │ │ ├── load_mayaascii.py │ │ ├── load_model.py │ │ ├── load_rig.py │ │ ├── load_setdress.py │ │ ├── load_vdb_to_arnold.py │ │ ├── load_vdb_to_redshift.py │ │ ├── load_vdb_to_vray.py │ │ ├── load_vrayproxy.py │ │ ├── load_yeti_cache.py │ │ └── load_yeti_rig.py │ │ └── publish │ │ ├── collect_animation.py │ │ ├── collect_current_file.py │ │ ├── collect_history.py │ │ ├── collect_instances.py │ │ ├── collect_look.py │ │ ├── collect_maya_units.py │ │ ├── collect_maya_workspace.py │ │ ├── collect_model.py │ │ ├── collect_render_layer_aovs.py │ │ ├── collect_renderable_camera.py │ │ ├── collect_renderlayers.py │ │ ├── collect_setdress.py │ │ ├── collect_vray_scene.py │ │ ├── collect_workscene_fps.py │ │ ├── collect_yeti_cache.py │ │ ├── collect_yeti_rig.py │ │ ├── extract_animation.py │ │ ├── extract_camera_alembic.py │ │ ├── extract_camera_mayaAscii.py │ │ ├── extract_fbx.py │ │ ├── extract_look.py │ │ ├── extract_maya_ascii_raw.py │ │ ├── extract_model.py │ │ ├── extract_pointcache.py │ │ ├── extract_rig.py │ │ ├── extract_setdress.py │ │ ├── extract_vrayproxy.py │ │ ├── extract_yeti_cache.py │ │ ├── extract_yeti_rig.py │ │ ├── increment_current_file_deadline.py │ │ ├── save_scene.py │ │ ├── submit_maya_deadline.py │ │ ├── submit_vray_deadline.py │ │ ├── validate_animation_content.py │ │ ├── validate_animation_out_set_related_node_ids.py │ │ ├── validate_arnold_layername.py │ │ ├── validate_camera_attributes.py │ │ ├── validate_camera_contents.py │ │ ├── validate_current_renderlayer_renderable.py │ │ ├── validate_deadline_connection.py │ │ ├── validate_frame_range.py │ │ ├── validate_instance_has_members.py │ │ ├── validate_instance_subset.py │ │ ├── validate_instancer_content.py │ │ ├── validate_instancer_frame_ranges.py │ │ ├── validate_joints_hidden.py │ │ ├── validate_look_contents.py │ │ ├── validate_look_default_shaders_connections.py │ │ ├── validate_look_id_reference_edits.py │ │ ├── validate_look_members_unique.py │ │ ├── validate_look_no_default_shaders.py │ │ ├── validate_look_sets.py │ │ ├── validate_look_single_shader.py │ │ ├── validate_maya_units.py │ │ ├── validate_mesh_has_uv.py │ │ ├── validate_mesh_lamina_faces.py │ │ ├── validate_mesh_no_negative_scale.py │ │ ├── validate_mesh_non_manifold.py │ │ ├── validate_mesh_non_zero_edge.py │ │ ├── validate_mesh_normals_unlocked.py │ │ ├── validate_mesh_shader_connections.py │ │ ├── validate_mesh_single_uv_set.py │ │ ├── validate_mesh_uv_set_map1.py │ │ ├── validate_mesh_vertices_have_edges.py │ │ ├── validate_model_content.py │ │ ├── validate_no_animation.py │ │ ├── validate_no_default_camera.py │ │ ├── validate_no_namespace.py │ │ ├── validate_no_null_transforms.py │ │ ├── validate_no_unknown_nodes.py │ │ ├── validate_no_vraymesh.py │ │ ├── validate_node_ids.py │ │ ├── validate_node_ids_deformed_shapes.py │ │ ├── validate_node_ids_in_database.py │ │ ├── validate_node_ids_related.py │ │ ├── validate_node_ids_unique.py │ │ ├── validate_node_no_ghosting.py │ │ ├── validate_render_image_rule.py │ │ ├── validate_render_no_default_cameras.py │ │ ├── validate_render_single_camera.py │ │ ├── validate_renderlayer_aovs.py │ │ ├── validate_rendersettings.py │ │ ├── validate_rig_contents.py │ │ ├── validate_rig_controllers.py │ │ ├── validate_rig_controllers_arnold_attributes.py │ │ ├── validate_rig_out_set_node_ids.py │ │ ├── validate_scene_set_workspace.py │ │ ├── validate_setdress_namespaces.py │ │ ├── validate_setdress_transforms.py │ │ ├── validate_shape_default_names.py │ │ ├── validate_shape_render_stats.py │ │ ├── validate_single_assembly.py │ │ ├── validate_skinCluster_deformer_set.py │ │ ├── validate_step_size.py │ │ ├── validate_transfers.py │ │ ├── validate_transform_naming_suffix.py │ │ ├── validate_transform_zero.py │ │ ├── validate_vray_distributed_rendering.py │ │ ├── validate_vray_translator_settings.py │ │ ├── validate_vrayproxy_members.py │ │ ├── validate_yeti_renderscript_callbacks.py │ │ ├── validate_yeti_rig_input_in_instance.py │ │ ├── validate_yeti_rig_settings.py │ │ └── validate_yetirig_cache_state.py ├── scripts │ ├── __init__.py │ ├── fusion_switch_shot.py │ └── publish_filesequence.py ├── setdress_api.py ├── vendor │ ├── __init__.py │ └── pather │ │ ├── __init__.py │ │ ├── core.py │ │ ├── error.py │ │ └── version.py ├── version.py └── widgets │ ├── __init__.py │ └── popup.py ├── res ├── icons │ ├── colorbleed_logo_36x36.png │ ├── inventory.png │ ├── loader.png │ └── workfiles.png └── workspace.mel ├── setup.cfg ├── setup.py └── setup └── fusion └── scripts └── Comp └── colorbleed ├── 32bit ├── backgrounds_selected_to32bit.py ├── backgrounds_to32bit.py ├── loaders_selected_to32bit.py └── loaders_to32bit.py ├── duplicate_with_input_connections.py ├── set_rendermode.py ├── switch_ui.py └── update_selected_loader_ranges.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # Pycharm IDE settings 99 | .idea 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The [Colorbleed](http://www.colorbleed.nl/) animation studio *config* for [Avalon](https://getavalon.github.io/) 2 | 3 |
4 | 5 | _This configuration is used for animation in film and advertising._ 6 | 7 | ### Code convention 8 | 9 | Below are some of the standard practices applied to this repositories. 10 | 11 | - **Etiquette: PEP8** 12 | - All code is written in PEP8. It is recommended you use a linter as you work, flake8 and pylinter are both good options. 13 | - **Etiquette: Napoleon docstrings** 14 | - Any docstrings are made in Google Napoleon format. See [Napoleon](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for details. 15 | - **Etiquette: Semantic Versioning** 16 | - This project follows [semantic versioning](http://semver.org). 17 | - **Etiquette: Underscore means private** 18 | - Anything prefixed with an underscore means that it is internal to wherever it is used. For example, a variable name is only ever used in the parent function or class. A module is not for use by the end-user. In contrast, anything without an underscore is public, but not necessarily part of the API. Members of the API resides in `api.py`. 19 | - **API: Idempotence** 20 | - A public function must be able to be called twice and produce the exact same result. This means no changing of state without restoring previous state when finishing. For example, if a function requires changing the current selection in Autodesk Maya, it must restore the previous selection prior to completing. -------------------------------------------------------------------------------- /colorbleed/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyblish import api as pyblish 4 | from avalon import api as avalon 5 | 6 | from .launcher_actions import register_launcher_actions 7 | from .lib import collect_container_metadata 8 | 9 | PACKAGE_DIR = os.path.dirname(__file__) 10 | PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") 11 | 12 | # Global plugin paths 13 | PUBLISH_PATH = os.path.join(PLUGINS_DIR, "global", "publish") 14 | LOAD_PATH = os.path.join(PLUGINS_DIR, "global", "load") 15 | 16 | 17 | def install(): 18 | print("Registering global plug-ins..") 19 | pyblish.register_plugin_path(PUBLISH_PATH) 20 | avalon.register_plugin_path(avalon.Loader, LOAD_PATH) 21 | 22 | 23 | def uninstall(): 24 | print("Deregistering global plug-ins..") 25 | pyblish.deregister_plugin_path(PUBLISH_PATH) 26 | avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) 27 | -------------------------------------------------------------------------------- /colorbleed/api.py: -------------------------------------------------------------------------------- 1 | from .plugin import ( 2 | 3 | Extractor, 4 | 5 | ValidatePipelineOrder, 6 | ValidateContentsOrder, 7 | ValidateSceneOrder, 8 | ValidateMeshOrder 9 | ) 10 | 11 | # temporary fix, might 12 | from .action import ( 13 | get_errored_instances_from_context, 14 | RepairAction, 15 | RepairContextAction 16 | ) 17 | 18 | __all__ = [ 19 | # plugin classes 20 | "Extractor", 21 | # ordering 22 | "ValidatePipelineOrder", 23 | "ValidateContentsOrder", 24 | "ValidateSceneOrder", 25 | "ValidateMeshOrder", 26 | # action 27 | "get_errored_instances_from_context", 28 | "RepairAction" 29 | ] 30 | -------------------------------------------------------------------------------- /colorbleed/fusion/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from avalon import api as avalon 4 | from pyblish import api as pyblish 5 | 6 | 7 | PARENT_DIR = os.path.dirname(__file__) 8 | PACKAGE_DIR = os.path.dirname(PARENT_DIR) 9 | PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") 10 | 11 | PUBLISH_PATH = os.path.join(PLUGINS_DIR, "fusion", "publish") 12 | LOAD_PATH = os.path.join(PLUGINS_DIR, "fusion", "load") 13 | CREATE_PATH = os.path.join(PLUGINS_DIR, "fusion", "create") 14 | INVENTORY_PATH = os.path.join(PLUGINS_DIR, "fusion", "inventory") 15 | 16 | 17 | def install(): 18 | print("Registering Fusion plug-ins..") 19 | pyblish.register_plugin_path(PUBLISH_PATH) 20 | avalon.register_plugin_path(avalon.Loader, LOAD_PATH) 21 | avalon.register_plugin_path(avalon.Creator, CREATE_PATH) 22 | avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) 23 | 24 | pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) 25 | 26 | # Disable all families except for the ones we explicitly want to see 27 | family_states = ["colorbleed.imagesequence", 28 | "colorbleed.camera", 29 | "colorbleed.pointcache"] 30 | 31 | avalon.data["familiesStateDefault"] = False 32 | avalon.data["familiesStateToggled"] = family_states 33 | 34 | 35 | def uninstall(): 36 | print("Deregistering Fusion plug-ins..") 37 | pyblish.deregister_plugin_path(PUBLISH_PATH) 38 | avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) 39 | avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) 40 | 41 | pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) 42 | 43 | 44 | def on_pyblish_instance_toggled(instance, new_value, old_value): 45 | """Toggle saver tool passthrough states on instance toggles.""" 46 | 47 | from avalon.fusion import comp_lock_and_undo_chunk 48 | 49 | comp = instance.context.data.get("currentComp") 50 | if not comp: 51 | return 52 | 53 | savers = [tool for tool in instance if 54 | getattr(tool, "ID", None) == "Saver"] 55 | if not savers: 56 | return 57 | 58 | # Whether instances should be passthrough based on new value 59 | passthrough = not new_value 60 | with comp_lock_and_undo_chunk(comp, 61 | undo_queue_name="Change instance " 62 | "active state"): 63 | for tool in savers: 64 | attrs = tool.GetAttrs() 65 | current = attrs["TOOLB_PassThrough"] 66 | if current != passthrough: 67 | tool.SetAttrs({"TOOLB_PassThrough": passthrough}) 68 | -------------------------------------------------------------------------------- /colorbleed/fusion/lib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from avalon.vendor.Qt import QtGui 4 | import avalon.fusion 5 | 6 | 7 | self = sys.modules[__name__] 8 | self._project = None 9 | 10 | 11 | def update_frame_range(start, end, comp=None, set_render_range=True): 12 | """Set Fusion comp's start and end frame range 13 | 14 | Args: 15 | start (float, int): start frame 16 | end (float, int): end frame 17 | comp (object, Optional): comp object from fusion 18 | set_render_range (bool, Optional): When True this will also set the 19 | composition's render start and end frame. 20 | 21 | Returns: 22 | None 23 | 24 | """ 25 | 26 | if not comp: 27 | comp = avalon.fusion.get_current_comp() 28 | 29 | attrs = { 30 | "COMPN_GlobalStart": start, 31 | "COMPN_GlobalEnd": end 32 | } 33 | 34 | if set_render_range: 35 | attrs.update({ 36 | "COMPN_RenderStart": start, 37 | "COMPN_RenderEnd": end 38 | }) 39 | 40 | with avalon.fusion.comp_lock_and_undo_chunk(comp): 41 | comp.SetAttrs(attrs) 42 | 43 | 44 | def get_additional_data(container): 45 | """Get Fusion related data for the container 46 | 47 | Args: 48 | container(dict): the container found by the ls() function 49 | 50 | Returns: 51 | dict 52 | """ 53 | 54 | tool = container["_tool"] 55 | tile_color = tool.TileColor 56 | if tile_color is None: 57 | return {} 58 | 59 | return {"color": QtGui.QColor.fromRgbF(tile_color["R"], 60 | tile_color["G"], 61 | tile_color["B"])} 62 | -------------------------------------------------------------------------------- /colorbleed/maya/menu.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | 5 | from avalon.vendor.Qt import QtWidgets, QtCore, QtGui 6 | 7 | import maya.cmds as cmds 8 | 9 | self = sys.modules[__name__] 10 | self._menu = "colorbleed" 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def _get_menu(): 16 | """Return the menu instance if it currently exists in Maya""" 17 | 18 | app = QtWidgets.QApplication.instance() 19 | widgets = dict((w.objectName(), w) for w in app.allWidgets()) 20 | menu = widgets.get(self._menu) 21 | return menu 22 | 23 | 24 | def deferred(): 25 | 26 | log.info("Attempting to install scripts menu..") 27 | 28 | try: 29 | import scriptsmenu.launchformaya as launchformaya 30 | import scriptsmenu.scriptsmenu as scriptsmenu 31 | except ImportError: 32 | log.warning("Skipping colorbleed.menu install, because " 33 | "'scriptsmenu' module seems unavailable.") 34 | return 35 | 36 | # load configuration of custom menu 37 | config_path = os.path.join(os.path.dirname(__file__), "menu.json") 38 | config = scriptsmenu.load_configuration(config_path) 39 | 40 | # run the launcher for Maya menu 41 | cb_menu = launchformaya.main(title=self._menu.title(), 42 | objectName=self._menu) 43 | 44 | # apply configuration 45 | cb_menu.build_from_configuration(cb_menu, config) 46 | 47 | 48 | def uninstall(): 49 | 50 | menu = _get_menu() 51 | if menu: 52 | log.info("Attempting to uninstall..") 53 | 54 | try: 55 | menu.deleteLater() 56 | del menu 57 | except Exception as e: 58 | log.error(e) 59 | 60 | 61 | def install(): 62 | 63 | if cmds.about(batch=True): 64 | print("Skipping colorbleed.menu initialization in batch mode..") 65 | return 66 | 67 | uninstall() 68 | # Allow time for uninstallation to finish. 69 | cmds.evalDeferred(deferred) 70 | 71 | 72 | def popup(): 73 | """Pop-up the existing menu near the mouse cursor""" 74 | menu = _get_menu() 75 | 76 | cursor = QtGui.QCursor() 77 | point = cursor.pos() 78 | menu.exec_(point) 79 | -------------------------------------------------------------------------------- /colorbleed/plugin.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import pyblish.api 3 | 4 | ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 5 | ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 6 | ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 7 | ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 8 | 9 | 10 | class Extractor(pyblish.api.InstancePlugin): 11 | """Extractor base class. 12 | 13 | The extractor base class implements a "staging_dir" function used to 14 | generate a temporary directory for an instance to extract to. 15 | 16 | This temporary directory is generated through `tempfile.mkdtemp()` 17 | 18 | """ 19 | 20 | order = 2.0 21 | 22 | def staging_dir(self, instance): 23 | """Provide a temporary directory in which to store extracted files 24 | 25 | Upon calling this method the staging directory is stored inside 26 | the instance.data['stagingDir'] 27 | """ 28 | staging_dir = instance.data.get('stagingDir', None) 29 | 30 | if not staging_dir: 31 | staging_dir = tempfile.mkdtemp(prefix="pyblish_tmp_") 32 | instance.data['stagingDir'] = staging_dir 33 | 34 | return staging_dir 35 | 36 | 37 | def contextplugin_should_run(plugin, context): 38 | """Return whether the ContextPlugin should run on the given context. 39 | 40 | This is a helper function to work around a bug pyblish-base#250 41 | Whenever a ContextPlugin sets specific families it will still trigger even 42 | when no instances are present that have those families. 43 | 44 | This actually checks it correctly and returns whether it should run. 45 | 46 | """ 47 | required = set(plugin.families) 48 | 49 | # When no filter always run 50 | if "*" in required: 51 | return True 52 | 53 | for instance in context: 54 | 55 | # Ignore inactive instances 56 | if (not instance.data.get("publish", True) or 57 | not instance.data.get("active", True)): 58 | continue 59 | 60 | families = instance.data.get("families", []) 61 | if any(f in required for f in families): 62 | return True 63 | 64 | family = instance.data.get("family") 65 | if family and family in required: 66 | return True 67 | 68 | return False 69 | 70 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/create/create_tiff_saver.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import avalon.api 4 | from avalon import fusion 5 | 6 | 7 | class CreateTiffSaver(avalon.api.Creator): 8 | 9 | label = "Create Tiff Saver" 10 | hosts = ["fusion"] 11 | family = "colorbleed.saver" 12 | 13 | def process(self): 14 | 15 | file_format = "TiffFormat" 16 | 17 | comp = fusion.get_current_comp() 18 | 19 | # todo: improve method of getting current environment 20 | # todo: pref avalon.Session over os.environ 21 | 22 | workdir = os.path.normpath(os.environ["AVALON_WORKDIR"]) 23 | 24 | filename = "{}..tiff".format(self.name) 25 | filepath = os.path.join(workdir, "render", "preview", filename) 26 | 27 | with fusion.comp_lock_and_undo_chunk(comp): 28 | args = (-32768, -32768) # Magical position numbers 29 | saver = comp.AddTool("Saver", *args) 30 | saver.SetAttrs({"TOOLS_Name": self.name}) 31 | 32 | # Setting input attributes is different from basic attributes 33 | # Not confused with "MainInputAttributes" which 34 | saver["Clip"] = filepath 35 | saver["OutputFormat"] = file_format 36 | 37 | # # # Set standard TIFF settings 38 | if saver[file_format] is None: 39 | raise RuntimeError("File format is not set to TiffFormat, " 40 | "this is a bug") 41 | 42 | # Set file format attributes 43 | saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other 44 | saver[file_format]["SaveAlpha"] = 0 45 | 46 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/inventory/select_containers.py: -------------------------------------------------------------------------------- 1 | from avalon import api 2 | 3 | 4 | class FusionSelectContainers(api.InventoryAction): 5 | 6 | label = "Select Containers" 7 | icon = "mouse-pointer" 8 | color = "#d8d8d8" 9 | 10 | def process(self, containers): 11 | 12 | import avalon.fusion 13 | 14 | tools = [i["_tool"] for i in containers] 15 | 16 | comp = avalon.fusion.get_current_comp() 17 | flow = comp.CurrentFrame.FlowView 18 | 19 | with avalon.fusion.comp_lock_and_undo_chunk(comp, self.label): 20 | # Clear selection 21 | flow.Select() 22 | 23 | # Select tool 24 | for tool in tools: 25 | flow.Select(tool) 26 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/inventory/set_tool_color.py: -------------------------------------------------------------------------------- 1 | from avalon import api, style 2 | from avalon.vendor.Qt import QtGui, QtWidgets 3 | 4 | import avalon.fusion 5 | 6 | 7 | class FusionSetToolColor(api.InventoryAction): 8 | """Update the color of the selected tools""" 9 | 10 | label = "Set Tool Color" 11 | icon = "plus" 12 | color = "#d8d8d8" 13 | _fallback_color = QtGui.QColor(1.0, 1.0, 1.0) 14 | 15 | def process(self, containers): 16 | """Color all selected tools the selected colors""" 17 | 18 | result = [] 19 | comp = avalon.fusion.get_current_comp() 20 | 21 | # Get tool color 22 | first = containers[0] 23 | tool = first["_tool"] 24 | color = tool.TileColor 25 | 26 | if color is not None: 27 | qcolor = QtGui.QColor().fromRgbF(color["R"], color["G"], color["B"]) 28 | else: 29 | qcolor = self._fallback_color 30 | 31 | # Launch pick color 32 | picked_color = self.get_color_picker(qcolor) 33 | if not picked_color: 34 | return 35 | 36 | with avalon.fusion.comp_lock_and_undo_chunk(comp): 37 | for container in containers: 38 | # Convert color to RGB 0-1 floats 39 | rgb_f = picked_color.getRgbF() 40 | rgb_f_table = {"R": rgb_f[0], "G": rgb_f[1], "B": rgb_f[2]} 41 | 42 | # Update tool 43 | tool = container["_tool"] 44 | tool.TileColor = rgb_f_table 45 | 46 | result.append(container) 47 | 48 | return result 49 | 50 | def get_color_picker(self, color): 51 | """Launch color picker and return chosen color 52 | 53 | Args: 54 | color(QtGui.QColor): Start color to display 55 | 56 | Returns: 57 | QtGui.QColor 58 | 59 | """ 60 | 61 | color_dialog = QtWidgets.QColorDialog(color) 62 | color_dialog.setStyleSheet(style.load_stylesheet()) 63 | 64 | accepted = color_dialog.exec_() 65 | if not accepted: 66 | return 67 | 68 | return color_dialog.selectedColor() 69 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/load/actions.py: -------------------------------------------------------------------------------- 1 | """A module containing generic loader actions that will display in the Loader. 2 | 3 | """ 4 | 5 | from avalon import api 6 | 7 | 8 | class FusionSetFrameRangeLoader(api.Loader): 9 | """Specific loader of Alembic for the avalon.animation family""" 10 | 11 | families = ["colorbleed.animation", 12 | "colorbleed.camera", 13 | "colorbleed.imagesequence", 14 | "colorbleed.yeticache", 15 | "colorbleed.pointcache"] 16 | representations = ["*"] 17 | 18 | label = "Set frame range" 19 | order = 11 20 | icon = "clock-o" 21 | color = "white" 22 | 23 | def load(self, context, name, namespace, data): 24 | 25 | from colorbleed.fusion import lib 26 | 27 | version = context['version'] 28 | version_data = version.get("data", {}) 29 | 30 | start = version_data.get("startFrame", None) 31 | end = version_data.get("endFrame", None) 32 | 33 | if start is None or end is None: 34 | print("Skipping setting frame range because start or " 35 | "end frame data is missing..") 36 | return 37 | 38 | lib.update_frame_range(start, end) 39 | 40 | 41 | class FusionSetFrameRangeWithHandlesLoader(api.Loader): 42 | """Specific loader of Alembic for the avalon.animation family""" 43 | 44 | families = ["colorbleed.animation", 45 | "colorbleed.camera", 46 | "colorbleed.imagesequence", 47 | "colorbleed.yeticache", 48 | "colorbleed.pointcache"] 49 | representations = ["*"] 50 | 51 | label = "Set frame range (with handles)" 52 | order = 12 53 | icon = "clock-o" 54 | color = "white" 55 | 56 | def load(self, context, name, namespace, data): 57 | 58 | from colorbleed.fusion import lib 59 | 60 | version = context['version'] 61 | version_data = version.get("data", {}) 62 | 63 | start = version_data.get("startFrame", None) 64 | end = version_data.get("endFrame", None) 65 | 66 | if start is None or end is None: 67 | print("Skipping setting frame range because start or " 68 | "end frame data is missing..") 69 | return 70 | 71 | # Include handles 72 | handles = version_data.get("handles", 0) 73 | start -= handles 74 | end += handles 75 | 76 | lib.update_frame_range(start, end) 77 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/collect_comp.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | 5 | from avalon import fusion 6 | 7 | 8 | class CollectCurrentCompFusion(pyblish.api.ContextPlugin): 9 | """Collect current comp""" 10 | 11 | order = pyblish.api.CollectorOrder - 0.4 12 | label = "Collect Current Comp" 13 | hosts = ["fusion"] 14 | 15 | def process(self, context): 16 | """Collect all image sequence tools""" 17 | 18 | current_comp = fusion.get_current_comp() 19 | assert current_comp, "Must have active Fusion composition" 20 | context.data["currentComp"] = current_comp 21 | 22 | # Store path to current file 23 | filepath = current_comp.GetAttrs().get("COMPS_FileName", "") 24 | context.data['currentFile'] = filepath 25 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/collect_fusion_version.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class CollectFusionVersion(pyblish.api.ContextPlugin): 5 | """Collect current comp""" 6 | 7 | order = pyblish.api.CollectorOrder 8 | label = "Collect Fusion Version" 9 | hosts = ["fusion"] 10 | 11 | def process(self, context): 12 | """Collect all image sequence tools""" 13 | 14 | comp = context.data.get("currentComp") 15 | if not comp: 16 | raise RuntimeError("No comp previously collected, unable to " 17 | "retrieve Fusion version.") 18 | 19 | version = comp.GetApp().Version 20 | context.data["fusionVersion"] = version 21 | 22 | self.log.info("Fusion version: %s" % version) 23 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/collect_render_target.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class CollectFusionRenderMode(pyblish.api.InstancePlugin): 5 | """Collect current comp's render Mode 6 | 7 | Options: 8 | renderlocal 9 | deadline 10 | 11 | Note that this value is set for each comp separately. When you save the 12 | comp this information will be stored in that file. If for some reason the 13 | available tool does not visualize which render mode is set for the 14 | current comp, please run the following line in the console (Py2) 15 | 16 | comp.GetData("colorbleed.rendermode") 17 | 18 | This will return the name of the current render mode as seen above under 19 | Options. 20 | 21 | """ 22 | 23 | order = pyblish.api.CollectorOrder + 0.4 24 | label = "Collect Render Mode" 25 | hosts = ["fusion"] 26 | families = ["colorbleed.saver"] 27 | 28 | def process(self, instance): 29 | """Collect all image sequence tools""" 30 | options = ["renderlocal", "deadline"] 31 | 32 | comp = instance.context.data.get("currentComp") 33 | if not comp: 34 | raise RuntimeError("No comp previously collected, unable to " 35 | "retrieve Fusion version.") 36 | 37 | rendermode = comp.GetData("colorbleed.rendermode") or "renderlocal" 38 | assert rendermode in options, "Must be supported render mode" 39 | 40 | self.log.info("Render mode: {0}".format(rendermode)) 41 | 42 | # Append family 43 | family = "colorbleed.saver.{0}".format(rendermode) 44 | instance.data["families"].append(family) 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/increment_current_file_deadline.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): 5 | """Increment the current file. 6 | 7 | Saves the current file with an increased version number. 8 | 9 | """ 10 | 11 | label = "Increment current file" 12 | order = pyblish.api.IntegratorOrder + 9.0 13 | hosts = ["fusion"] 14 | families = ["colorbleed.saver.deadline"] 15 | optional = True 16 | 17 | def process(self, context): 18 | 19 | from colorbleed.lib import version_up 20 | from colorbleed.action import get_errored_plugins_from_data 21 | 22 | errored_plugins = get_errored_plugins_from_data(context) 23 | if any(plugin.__name__ == "FusionSubmitDeadline" 24 | for plugin in errored_plugins): 25 | raise RuntimeError("Skipping incrementing current file because " 26 | "submission to deadline failed.") 27 | 28 | comp = context.data.get("currentComp") 29 | assert comp, "Must have comp" 30 | 31 | current_filepath = context.data["currentFile"] 32 | new_filepath = version_up(current_filepath) 33 | 34 | comp.Save(new_filepath) 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/render_local.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import avalon.fusion as fusion 4 | 5 | 6 | class FusionRenderLocal(pyblish.api.InstancePlugin): 7 | """Render the current Fusion composition locally. 8 | 9 | Extract the result of savers by starting a comp render 10 | This will run the local render of Fusion. 11 | 12 | """ 13 | 14 | order = pyblish.api.ExtractorOrder 15 | label = "Render Local" 16 | hosts = ["fusion"] 17 | families = ["colorbleed.saver.renderlocal"] 18 | 19 | def process(self, instance): 20 | 21 | # This should be a ContextPlugin, but this is a workaround 22 | # for a bug in pyblish to run once for a family: issue #250 23 | context = instance.context 24 | key = "__hasRun{}".format(self.__class__.__name__) 25 | if context.data.get(key, False): 26 | return 27 | else: 28 | context.data[key] = True 29 | 30 | current_comp = context.data["currentComp"] 31 | start_frame = current_comp.GetAttrs("COMPN_RenderStart") 32 | end_frame = current_comp.GetAttrs("COMPN_RenderEnd") 33 | 34 | self.log.info("Starting render") 35 | self.log.info("Start frame: {}".format(start_frame)) 36 | self.log.info("End frame: {}".format(end_frame)) 37 | 38 | with fusion.comp_lock_and_undo_chunk(current_comp): 39 | result = current_comp.Render() 40 | 41 | if not result: 42 | raise RuntimeError("Comp render failed") 43 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/save_scene.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class FusionSaveComp(pyblish.api.ContextPlugin): 5 | """Save current comp""" 6 | 7 | label = "Save current file" 8 | order = pyblish.api.ExtractorOrder - 0.49 9 | hosts = ["fusion"] 10 | families = ["colorbleed.saver"] 11 | 12 | def process(self, context): 13 | 14 | comp = context.data.get("currentComp") 15 | assert comp, "Must have comp" 16 | 17 | current = comp.GetAttrs().get("COMPS_FileName", "") 18 | assert context.data['currentFile'] == current 19 | 20 | self.log.info("Saving current file..") 21 | comp.Save() 22 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_background_depth.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | from colorbleed import action 4 | 5 | 6 | class ValidateBackgroundDepth(pyblish.api.InstancePlugin): 7 | """Validate if all Background tool are set to float32 bit""" 8 | 9 | order = pyblish.api.ValidatorOrder 10 | label = "Validate Background Depth 32 bit" 11 | actions = [action.RepairAction] 12 | hosts = ["fusion"] 13 | families = ["colorbleed.saver"] 14 | optional = True 15 | 16 | @classmethod 17 | def get_invalid(cls, instance): 18 | 19 | context = instance.context 20 | comp = context.data.get("currentComp") 21 | assert comp, "Must have Comp object" 22 | 23 | backgrounds = comp.GetToolList(False, "Background").values() 24 | if not backgrounds: 25 | return [] 26 | 27 | return [i for i in backgrounds if i.GetInput("Depth") != 4.0] 28 | 29 | def process(self, instance): 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Found %i nodes which are not set to float32" 33 | % len(invalid)) 34 | 35 | @classmethod 36 | def repair(cls, instance): 37 | comp = instance.context.data.get("currentComp") 38 | invalid = cls.get_invalid(instance) 39 | for i in invalid: 40 | i.SetInput("Depth", 4.0, comp.TIME_UNDEFINED) 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_comp_saved.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | 5 | 6 | class ValidateFusionCompSaved(pyblish.api.ContextPlugin): 7 | """Ensure current comp is saved""" 8 | 9 | order = pyblish.api.ValidatorOrder 10 | label = "Validate Comp Saved" 11 | families = ["colorbleed.saver"] 12 | hosts = ["fusion"] 13 | 14 | def process(self, context): 15 | 16 | comp = context.data.get("currentComp") 17 | assert comp, "Must have Comp object" 18 | attrs = comp.GetAttrs() 19 | 20 | filename = attrs["COMPS_FileName"] 21 | if not filename: 22 | raise RuntimeError("Comp is not saved.") 23 | 24 | if not os.path.exists(filename): 25 | raise RuntimeError("Comp file does not exist: %s" % filename) 26 | 27 | if attrs["COMPB_Modified"]: 28 | self.log.warning("Comp is modified. Save your comp to ensure your " 29 | "changes propagate correctly.") 30 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_create_folder_checked.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | from colorbleed import action 4 | 5 | 6 | class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): 7 | """Valid if all savers have the input attribute CreateDir checked on 8 | 9 | This attribute ensures that the folders to which the saver will write 10 | will be created. 11 | """ 12 | 13 | order = pyblish.api.ValidatorOrder 14 | actions = [action.RepairAction] 15 | label = "Validate Create Folder Checked" 16 | families = ["colorbleed.saver"] 17 | hosts = ["fusion"] 18 | 19 | @classmethod 20 | def get_invalid(cls, instance): 21 | active = instance.data.get("active", instance.data.get("publish")) 22 | if not active: 23 | return [] 24 | 25 | tool = instance[0] 26 | create_dir = tool.GetInput("CreateDir") 27 | if create_dir == 0.0: 28 | cls.log.error("%s has Create Folder turned off" % instance[0].Name) 29 | return [tool] 30 | 31 | def process(self, instance): 32 | invalid = self.get_invalid(instance) 33 | if invalid: 34 | raise RuntimeError("Found Saver with Create Folder During " 35 | "Render checked off") 36 | 37 | @classmethod 38 | def repair(cls, instance): 39 | invalid = cls.get_invalid(instance) 40 | for tool in invalid: 41 | tool.SetInput("CreateDir", 1.0) 42 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_filename_has_extension.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | 5 | 6 | class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): 7 | """Ensure the Saver has an extension in the filename path 8 | 9 | This disallows files written as `filename` instead of `filename.frame.ext`. 10 | Fusion does not always set an extension for your filename when 11 | changing the file format of the saver. 12 | 13 | """ 14 | 15 | order = pyblish.api.ValidatorOrder 16 | label = "Validate Filename Has Extension" 17 | families = ["colorbleed.saver"] 18 | hosts = ["fusion"] 19 | 20 | def process(self, instance): 21 | invalid = self.get_invalid(instance) 22 | if invalid: 23 | raise RuntimeError("Found Saver without an extension") 24 | 25 | @classmethod 26 | def get_invalid(cls, instance): 27 | 28 | path = instance.data["path"] 29 | fname, ext = os.path.splitext(path) 30 | 31 | if not ext: 32 | tool = instance[0] 33 | cls.log.error("%s has no extension specified" % tool.Name) 34 | return [tool] 35 | 36 | return [] 37 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_saver_has_input.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateSaverHasInput(pyblish.api.InstancePlugin): 5 | """Validate saver has incoming connection 6 | 7 | This ensures a Saver has at least an input connection. 8 | 9 | """ 10 | 11 | order = pyblish.api.ValidatorOrder 12 | label = "Validate Saver Has Input" 13 | families = ["colorbleed.saver"] 14 | hosts = ["fusion"] 15 | 16 | @classmethod 17 | def get_invalid(cls, instance): 18 | 19 | saver = instance[0] 20 | if not saver.Input.GetConnectedOutput(): 21 | return [saver] 22 | 23 | return [] 24 | 25 | def process(self, instance): 26 | invalid = self.get_invalid(instance) 27 | if invalid: 28 | raise RuntimeError("Saver has no incoming connection: " 29 | "{} ({})".format(instance, invalid[0].Name)) 30 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_saver_passthrough.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateSaverPassthrough(pyblish.api.ContextPlugin): 5 | """Validate saver passthrough is similar to Pyblish publish state""" 6 | 7 | order = pyblish.api.ValidatorOrder 8 | label = "Validate Saver Passthrough" 9 | families = ["colorbleed.saver"] 10 | hosts = ["fusion"] 11 | 12 | def process(self, context): 13 | 14 | # Workaround for ContextPlugin always running, even if no instance 15 | # is present with the family 16 | instances = pyblish.api.instances_by_plugin(instances=list(context), 17 | plugin=self) 18 | if not instances: 19 | self.log.debug("Ignoring plugin.. (bugfix)") 20 | 21 | invalid_instances = [] 22 | for instance in instances: 23 | invalid = self.is_invalid(instance) 24 | if invalid: 25 | invalid_instances.append(instance) 26 | 27 | if invalid_instances: 28 | self.log.info("Reset pyblish to collect your current scene state, " 29 | "that should fix error.") 30 | raise RuntimeError("Invalid instances: " 31 | "{0}".format(invalid_instances)) 32 | 33 | def is_invalid(self, instance): 34 | 35 | saver = instance[0] 36 | attr = saver.GetAttrs() 37 | active = not attr["TOOLB_PassThrough"] 38 | 39 | if active != instance.data["publish"]: 40 | self.log.info("Saver has different passthrough state than " 41 | "Pyblish: {} ({})".format(instance, saver.Name)) 42 | return [saver] 43 | 44 | return [] 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/fusion/publish/validate_unique_subsets.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateUniqueSubsets(pyblish.api.InstancePlugin): 5 | """Ensure all instances have a unique subset name""" 6 | 7 | order = pyblish.api.ValidatorOrder 8 | label = "Validate Unique Subsets" 9 | families = ["colorbleed.saver"] 10 | hosts = ["fusion"] 11 | 12 | @classmethod 13 | def get_invalid(cls, instance): 14 | 15 | context = instance.context 16 | subset = instance.data["subset"] 17 | for other_instance in context[:]: 18 | if other_instance == instance: 19 | continue 20 | 21 | if other_instance.data["subset"] == subset: 22 | return [instance] # current instance is invalid 23 | 24 | return [] 25 | 26 | def process(self, instance): 27 | invalid = self.get_invalid(instance) 28 | if invalid: 29 | raise RuntimeError("Animation content is invalid. See log.") 30 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/load/copy_file.py: -------------------------------------------------------------------------------- 1 | from avalon import api 2 | 3 | 4 | class CopyFile(api.Loader): 5 | """Copy the published file to be pasted at the desired location""" 6 | 7 | representations = ["*"] 8 | families = ["*"] 9 | 10 | label = "Copy File" 11 | order = 99 12 | icon = "copy" 13 | color = "#666666" 14 | 15 | def load(self, context, name=None, namespace=None, data=None): 16 | self.log.info("Added copy to clipboard: {0}".format(self.fname)) 17 | self.copy_file_to_clipboard(self.fname) 18 | 19 | @staticmethod 20 | def copy_file_to_clipboard(path): 21 | from avalon.vendor.Qt import QtCore, QtWidgets 22 | 23 | app = QtWidgets.QApplication.instance() 24 | assert app, "Must have running QApplication instance" 25 | 26 | # Build mime data for clipboard 27 | data = QtCore.QMimeData() 28 | url = QtCore.QUrl.fromLocalFile(path) 29 | data.setUrls([url]) 30 | 31 | # Set to Clipboard 32 | clipboard = app.clipboard() 33 | clipboard.setMimeData(data) 34 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/load/copy_file_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from avalon import api 4 | 5 | 6 | class CopyFilePath(api.Loader): 7 | """Copy published file path to clipboard""" 8 | representations = ["*"] 9 | families = ["*"] 10 | 11 | label = "Copy File Path" 12 | order = 98 13 | icon = "clipboard" 14 | color = "#888888" 15 | 16 | def load(self, context, name=None, namespace=None, data=None): 17 | self.log.info("Added file path to clipboard: {0}".format(self.fname)) 18 | self.copy_path_to_clipboard(self.fname) 19 | 20 | @staticmethod 21 | def copy_path_to_clipboard(path): 22 | from avalon.vendor.Qt import QtCore, QtWidgets 23 | 24 | app = QtWidgets.QApplication.instance() 25 | assert app, "Must have running QApplication instance" 26 | 27 | # Set to Clipboard 28 | clipboard = app.clipboard() 29 | clipboard.setText(os.path.normpath(path)) 30 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/load/open_imagesequence.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | 5 | from avalon import api 6 | 7 | 8 | def open(filepath): 9 | """Open file with system default executable""" 10 | if sys.platform.startswith('darwin'): 11 | subprocess.call(('open', filepath)) 12 | elif os.name == 'nt': 13 | os.startfile(filepath) 14 | elif os.name == 'posix': 15 | subprocess.call(('xdg-open', filepath)) 16 | 17 | 18 | class PlayImageSequence(api.Loader): 19 | """Open Image Sequence with system default""" 20 | 21 | families = ["colorbleed.imagesequence"] 22 | representations = ["*"] 23 | 24 | label = "Play sequence" 25 | order = 30 26 | icon = "play-circle" 27 | color = "orange" 28 | 29 | def load(self, context, name, namespace, data): 30 | 31 | directory = self.fname 32 | from avalon.vendor import clique 33 | 34 | pattern = clique.PATTERNS["frames"] 35 | files = os.listdir(directory) 36 | collections, remainder = clique.assemble(files, 37 | patterns=[pattern], 38 | minimum_items=1) 39 | 40 | assert not remainder, ("There shouldn't have been a remainder for " 41 | "'%s': %s" % (directory, remainder)) 42 | 43 | seqeunce = collections[0] 44 | first_image = list(seqeunce)[0] 45 | filepath = os.path.normpath(os.path.join(directory, first_image)) 46 | 47 | self.log.info("Opening : {}".format(filepath)) 48 | 49 | open(filepath) 50 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/cleanup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import pyblish.api 4 | 5 | 6 | class CleanUp(pyblish.api.InstancePlugin): 7 | """Cleans up the staging directory after a successful publish. 8 | 9 | The removal will only happen for staging directories which are inside the 10 | temporary folder, otherwise the folder is ignored. 11 | 12 | """ 13 | 14 | order = pyblish.api.IntegratorOrder + 10 15 | label = "Clean Up" 16 | 17 | def process(self, instance): 18 | 19 | import tempfile 20 | 21 | staging_dir = instance.data.get("stagingDir", None) 22 | if not staging_dir or not os.path.exists(staging_dir): 23 | self.log.info("No staging directory found: %s" % staging_dir) 24 | return 25 | 26 | temp_root = tempfile.gettempdir() 27 | if not os.path.normpath(staging_dir).startswith(temp_root): 28 | self.log.info("Skipping cleanup. Staging directory is not in the " 29 | "temp folder: %s" % staging_dir) 30 | return 31 | 32 | self.log.info("Removing temporary folder ...") 33 | shutil.rmtree(staging_dir) 34 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_comment.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class CollectColorbleedComment(pyblish.api.ContextPlugin): 5 | """This plug-ins displays the comment dialog box per default""" 6 | 7 | label = "Collect Comment" 8 | order = pyblish.api.CollectorOrder 9 | 10 | def process(self, context): 11 | context.data["comment"] = "" 12 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_context_label.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyblish.api 3 | 4 | 5 | class CollectContextLabel(pyblish.api.ContextPlugin): 6 | """Labelize context using the registered host and current file""" 7 | 8 | order = pyblish.api.CollectorOrder + 0.25 9 | label = "Context Label" 10 | 11 | def process(self, context): 12 | 13 | # Get last registered host 14 | host = pyblish.api.registered_hosts()[-1] 15 | 16 | # Get scene name from "currentFile" 17 | path = context.data.get("currentFile") or "" 18 | base = os.path.basename(path) 19 | 20 | # Set label 21 | label = "{host} - {scene}".format(host=host.title(), scene=base) 22 | context.data["label"] = label 23 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_current_shell_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyblish.api 3 | 4 | 5 | class CollectCurrentShellFile(pyblish.api.ContextPlugin): 6 | """Inject the current working file into context""" 7 | 8 | order = pyblish.api.CollectorOrder - 0.5 9 | label = "Current File" 10 | hosts = ["shell"] 11 | 12 | def process(self, context): 13 | """Inject the current working file""" 14 | context.data["currentFile"] = os.path.join(os.getcwd(), "") 15 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_deadline_user.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import pyblish.api 5 | from colorbleed.plugin import contextplugin_should_run 6 | 7 | CREATE_NO_WINDOW = 0x08000000 8 | 9 | 10 | def deadline_command(cmd): 11 | # Find Deadline 12 | path = os.environ.get("DEADLINE_PATH", None) 13 | assert path is not None, "Variable 'DEADLINE_PATH' must be set" 14 | 15 | executable = os.path.join(path, "deadlinecommand") 16 | if os.name == "nt": 17 | executable += ".exe" 18 | assert os.path.exists( 19 | executable), "Deadline executable not found at %s" % executable 20 | assert cmd, "Must have a command" 21 | 22 | query = (executable, cmd) 23 | 24 | process = subprocess.Popen(query, stdout=subprocess.PIPE, 25 | stderr=subprocess.PIPE, 26 | universal_newlines=True, 27 | creationflags=CREATE_NO_WINDOW) 28 | out, err = process.communicate() 29 | 30 | return out 31 | 32 | 33 | class CollectDeadlineUser(pyblish.api.ContextPlugin): 34 | """Retrieve the local active Deadline user""" 35 | 36 | order = pyblish.api.CollectorOrder + 0.499 37 | label = "Deadline User" 38 | hosts = ['maya', 'fusion'] 39 | families = ["colorbleed.renderlayer", "colorbleed.saver.deadline"] 40 | 41 | def process(self, context): 42 | """Inject the current working file""" 43 | 44 | # Workaround bug pyblish-base#250 45 | if not contextplugin_should_run(self, context): 46 | return 47 | 48 | user = deadline_command("GetCurrentUserName").strip() 49 | 50 | if not user: 51 | self.log.warning("No Deadline user found. " 52 | "Do you have Deadline installed?") 53 | return 54 | 55 | self.log.info("Found Deadline user: {}".format(user)) 56 | context.data['deadlineUser'] = user 57 | 58 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_machine_name.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class CollectMachineName(pyblish.api.ContextPlugin): 5 | label = "Local Machine Name" 6 | order = pyblish.api.CollectorOrder 7 | hosts = ["*"] 8 | 9 | def process(self, context): 10 | import socket 11 | 12 | machine_name = socket.gethostname() 13 | self.log.info("Machine name: %s" % machine_name) 14 | context.data["machine"] = machine_name 15 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_project_code.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import avalon.io as io 3 | 4 | class CollectProjectCode(pyblish.api.ContextPlugin): 5 | """Collect the Project code from database project.data["config"] 6 | 7 | If project code not present in project.data it will fall back to None. 8 | 9 | """ 10 | 11 | label = "Collect Project Code" 12 | order = pyblish.api.CollectorOrder 13 | 14 | def process(self, context): 15 | 16 | project = io.find_one({"type": "project"}, 17 | projection={"data.code": True}) 18 | if not project: 19 | raise RuntimeError("Can't find current project in database.") 20 | 21 | code = project["data"].get("code", None) 22 | self.log.info("Collected project code: %s" % code) 23 | context.data["code"] = code 24 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_shell_workspace.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyblish.api 3 | 4 | 5 | class CollectShellWorkspace(pyblish.api.ContextPlugin): 6 | """Inject the current workspace into context""" 7 | 8 | order = pyblish.api.CollectorOrder - 0.5 9 | label = "Shell Workspace" 10 | 11 | hosts = ["shell"] 12 | 13 | def process(self, context): 14 | context.data["workspaceDir"] = os.getcwd() 15 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/collect_time.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | from avalon import api 3 | 4 | 5 | class CollectMindbenderTime(pyblish.api.ContextPlugin): 6 | """Store global time at the time of publish""" 7 | 8 | label = "Collect Current Time" 9 | order = pyblish.api.CollectorOrder 10 | 11 | def process(self, context): 12 | context.data["time"] = api.time() 13 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/validate_file_saved.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): 5 | """File must be saved before publishing""" 6 | 7 | label = "Validate File Saved" 8 | order = pyblish.api.ValidatorOrder - 0.1 9 | hosts = ["maya", "houdini"] 10 | 11 | def process(self, context): 12 | 13 | current_file = context.data["currentFile"] 14 | if not current_file: 15 | raise RuntimeError("File not saved") 16 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/validate_resources.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | import os 5 | 6 | 7 | class ValidateResources(pyblish.api.InstancePlugin): 8 | """Validates mapped resources. 9 | 10 | These are external files to the current application, for example 11 | these could be textures, image planes, cache files or other linked 12 | media. 13 | 14 | This validates: 15 | - The resources are existing files. 16 | - The resources["files"] must only contain (existing) files 17 | - The resources have correctly collected the data. 18 | 19 | """ 20 | 21 | order = colorbleed.api.ValidateContentsOrder 22 | label = "Resources" 23 | 24 | def process(self, instance): 25 | 26 | for resource in instance.data.get('resources', []): 27 | 28 | # Ensure required "source" in resource 29 | assert "source" in resource, ( 30 | "No source found in resource: %s" % resource 31 | ) 32 | 33 | # Ensure required "files" in resource 34 | assert "files" in resource, ( 35 | "No files from resource: %s" % resource 36 | ) 37 | 38 | # Detect paths that are not a file or don't exist 39 | not_files = [f for f in resource["files"] if not os.path.isfile(f)] 40 | assert not not_files, ( 41 | "Found non-files or non-existing files: %s (resource: %s)" % ( 42 | not_files, resource 43 | ) 44 | ) 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/global/publish/validate_sequence_frames.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateSequenceFrames(pyblish.api.InstancePlugin): 5 | """Ensure the sequence of frames is complete 6 | 7 | The files found in the folder are checked against the startFrame and 8 | endFrame of the instance. If the first or last file is not 9 | corresponding with the first or last frame it is flagged as invalid. 10 | """ 11 | 12 | order = pyblish.api.ValidatorOrder 13 | label = "Validate Sequence Frames" 14 | families = ["colorbleed.imagesequence"] 15 | hosts = ["shell"] 16 | 17 | def process(self, instance): 18 | 19 | collection = instance[0] 20 | self.log.info(collection) 21 | 22 | frames = list(collection.indexes) 23 | 24 | current_range = (frames[0], frames[-1]) 25 | required_range = (instance.data["startFrame"], 26 | instance.data["endFrame"]) 27 | 28 | if current_range != required_range: 29 | raise ValueError("Invalid frame range: {0} - " 30 | "expected: {1}".format(current_range, 31 | required_range)) 32 | 33 | missing = collection.holes().indexes 34 | assert not missing, "Missing frames: %s" % (missing,) 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/create/create_alembic_camera.py: -------------------------------------------------------------------------------- 1 | from avalon import houdini 2 | 3 | 4 | class CreateAlembicCamera(houdini.Creator): 5 | """Single baked camera from Alembic ROP""" 6 | 7 | label = "Camera (Abc)" 8 | family = "colorbleed.camera" 9 | icon = "camera" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreateAlembicCamera, self).__init__(*args, **kwargs) 13 | 14 | # Remove the active, we are checking the bypass flag of the nodes 15 | self.data.pop("active", None) 16 | 17 | # Set node type to create for output 18 | self.data.update({"node_type": "alembic"}) 19 | 20 | def process(self): 21 | instance = super(CreateAlembicCamera, self).process() 22 | 23 | parms = { 24 | "filename": "$HIP/pyblish/%s.abc" % self.name, 25 | "use_sop_path": False 26 | } 27 | 28 | if self.nodes: 29 | node = self.nodes[0] 30 | path = node.path() 31 | 32 | # Split the node path into the first root and the remainder 33 | # So we can set the root and objects parameters correctly 34 | _, root, remainder = path.split("/", 2) 35 | parms.update({ 36 | "root": "/" + root, 37 | "objects": remainder 38 | }) 39 | 40 | instance.setParms(parms) 41 | 42 | # Lock the Use Sop Path setting so the 43 | # user doesn't accidentally enable it. 44 | instance.parm("use_sop_path").lock(True) 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/create/create_pointcache.py: -------------------------------------------------------------------------------- 1 | from avalon import houdini 2 | 3 | 4 | class CreatePointCache(houdini.Creator): 5 | """Alembic ROP to pointcache""" 6 | 7 | label = "Point Cache" 8 | family = "colorbleed.pointcache" 9 | icon = "gears" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreatePointCache, self).__init__(*args, **kwargs) 13 | 14 | # Remove the active, we are checking the bypass flag of the nodes 15 | self.data.pop("active", None) 16 | 17 | self.data.update({"node_type": "alembic"}) 18 | 19 | def process(self): 20 | instance = super(CreatePointCache, self).process() 21 | 22 | parms = {"use_sop_path": True, # Export single node from SOP Path 23 | "build_from_path": True, # Direct path of primitive in output 24 | "path_attrib": "path", # Pass path attribute for output 25 | "prim_to_detail_pattern": "cbId", 26 | "format": 2, # Set format to Ogawa 27 | "filename": "$HIP/pyblish/%s.abc" % self.name} 28 | 29 | if self.nodes: 30 | node = self.nodes[0] 31 | parms.update({"sop_path": node.path()}) 32 | 33 | instance.setParms(parms) 34 | 35 | # Lock any parameters in this list 36 | to_lock = ["prim_to_detail_pattern"] 37 | for name in to_lock: 38 | parm = instance.parm(name) 39 | parm.lock(True) 40 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/create/create_vbd_cache.py: -------------------------------------------------------------------------------- 1 | from avalon import houdini 2 | 3 | 4 | class CreateVDBCache(houdini.Creator): 5 | """OpenVDB from Geometry ROP""" 6 | 7 | label = "VDB Cache" 8 | family = "colorbleed.vdbcache" 9 | icon = "cloud" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreateVDBCache, self).__init__(*args, **kwargs) 13 | 14 | # Remove the active, we are checking the bypass flag of the nodes 15 | self.data.pop("active", None) 16 | 17 | # Set node type to create for output 18 | self.data["node_type"] = "geometry" 19 | 20 | def process(self): 21 | instance = super(CreateVDBCache, self).process() 22 | 23 | parms = {"sopoutput": "$HIP/pyblish/%s.$F4.vdb" % self.name, 24 | "initsim": True} 25 | 26 | if self.nodes: 27 | node = self.nodes[0] 28 | parms.update({"soppath": node.path()}) 29 | 30 | instance.setParms(parms) 31 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/load/actions.py: -------------------------------------------------------------------------------- 1 | """A module containing generic loader actions that will display in the Loader. 2 | 3 | """ 4 | 5 | from avalon import api 6 | 7 | 8 | class SetFrameRangeLoader(api.Loader): 9 | """Set Maya frame range""" 10 | 11 | families = ["colorbleed.animation", 12 | "colorbleed.camera", 13 | "colorbleed.pointcache", 14 | "colorbleed.vdbcache"] 15 | representations = ["abc", "vdb"] 16 | 17 | label = "Set frame range" 18 | order = 11 19 | icon = "clock-o" 20 | color = "white" 21 | 22 | def load(self, context, name, namespace, data): 23 | 24 | import hou 25 | 26 | version = context['version'] 27 | version_data = version.get("data", {}) 28 | 29 | start = version_data.get("startFrame", None) 30 | end = version_data.get("endFrame", None) 31 | 32 | if start is None or end is None: 33 | print("Skipping setting frame range because start or " 34 | "end frame data is missing..") 35 | return 36 | 37 | hou.playbar.setFrameRange(start, end) 38 | hou.playbar.setPlaybackRange(start, end) 39 | 40 | 41 | class SetFrameRangeWithHandlesLoader(api.Loader): 42 | """Set Maya frame range including pre- and post-handles""" 43 | 44 | families = ["colorbleed.animation", 45 | "colorbleed.camera", 46 | "colorbleed.pointcache", 47 | "colorbleed.vdbcache"] 48 | representations = ["abc", "vdb"] 49 | 50 | label = "Set frame range (with handles)" 51 | order = 12 52 | icon = "clock-o" 53 | color = "white" 54 | 55 | def load(self, context, name, namespace, data): 56 | 57 | import hou 58 | 59 | version = context['version'] 60 | version_data = version.get("data", {}) 61 | 62 | start = version_data.get("startFrame", None) 63 | end = version_data.get("endFrame", None) 64 | 65 | if start is None or end is None: 66 | print("Skipping setting frame range because start or " 67 | "end frame data is missing..") 68 | return 69 | 70 | # Include handles 71 | handles = version_data.get("handles", 0) 72 | start -= handles 73 | end += handles 74 | 75 | hou.playbar.setFrameRange(start, end) 76 | hou.playbar.setPlaybackRange(start, end) 77 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/collect_current_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hou 3 | 4 | import pyblish.api 5 | 6 | 7 | class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): 8 | """Inject the current working file into context""" 9 | 10 | order = pyblish.api.CollectorOrder - 0.5 11 | label = "Houdini Current File" 12 | hosts = ['houdini'] 13 | 14 | def process(self, context): 15 | """Inject the current working file""" 16 | 17 | filepath = hou.hipFile.path() 18 | if not os.path.exists(filepath): 19 | # By default Houdini will even point a new scene to a path. 20 | # However if the file is not saved at all and does not exist, 21 | # we assume the user never set it. 22 | filepath = "" 23 | 24 | elif os.path.basename(filepath) == "untitled.hip": 25 | # Due to even a new file being called 'untitled.hip' we are unable 26 | # to confirm the current scene was ever saved because the file 27 | # could have existed already. We will allow it if the file exists, 28 | # but show a warning for this edge case to clarify the potential 29 | # false positive. 30 | self.log.warning("Current file is 'untitled.hip' and we are " 31 | "unable to detect whether the current scene is " 32 | "saved correctly.") 33 | 34 | context.data['currentFile'] = filepath 35 | 36 | 37 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/collect_frames.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import pyblish.api 5 | from colorbleed.houdini import lib 6 | 7 | 8 | class CollectFrames(pyblish.api.InstancePlugin): 9 | """Collect all frames which would be a resukl""" 10 | 11 | order = pyblish.api.CollectorOrder 12 | label = "Collect Frames" 13 | families = ["colorbleed.vdbcache"] 14 | 15 | def process(self, instance): 16 | 17 | ropnode = instance[0] 18 | 19 | output_parm = lib.get_output_parameter(ropnode) 20 | output = output_parm.eval() 21 | 22 | file_name = os.path.basename(output) 23 | match = re.match("(\w+)\.(\d+)\.vdb", file_name) 24 | result = file_name 25 | 26 | start_frame = instance.data.get("startFrame", None) 27 | end_frame = instance.data.get("endFrame", None) 28 | 29 | if match and start_frame is not None: 30 | 31 | # Check if frames are bigger than 1 (file collection) 32 | # override the result 33 | if end_frame - start_frame > 1: 34 | result = self.create_file_list(match, 35 | int(start_frame), 36 | int(end_frame)) 37 | 38 | instance.data.update({"frames": result}) 39 | 40 | def create_file_list(self, match, start_frame, end_frame): 41 | """Collect files based on frame range and regex.match 42 | 43 | Args: 44 | match(re.match): match object 45 | start_frame(int): start of the animation 46 | end_frame(int): end of the animation 47 | 48 | Returns: 49 | list 50 | 51 | """ 52 | 53 | result = [] 54 | 55 | padding = len(match.group(2)) 56 | name = match.group(1) 57 | padding_format = "{number:0{width}d}" 58 | 59 | count = start_frame 60 | while count <= end_frame: 61 | str_count = padding_format.format(number=count, width=padding) 62 | file_name = "{}.{}.vdb".format(name, str_count) 63 | result.append(file_name) 64 | count += 1 65 | 66 | return result 67 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/collect_output_node.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class CollectOutputSOPPath(pyblish.api.InstancePlugin): 5 | """Collect the out node's SOP Path value.""" 6 | 7 | order = pyblish.api.CollectorOrder 8 | families = ["colorbleed.pointcache", 9 | "colorbleed.vdbcache"] 10 | hosts = ["houdini"] 11 | label = "Collect Output SOP Path" 12 | 13 | def process(self, instance): 14 | 15 | import hou 16 | 17 | node = instance[0] 18 | 19 | # Get sop path 20 | if node.type().name() == "alembic": 21 | sop_path_parm = "sop_path" 22 | else: 23 | sop_path_parm = "soppath" 24 | 25 | sop_path = node.parm(sop_path_parm).eval() 26 | out_node = hou.node(sop_path) 27 | 28 | instance.data["output_node"] = out_node 29 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/collect_workscene_fps.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import hou 3 | 4 | 5 | class CollectWorksceneFPS(pyblish.api.ContextPlugin): 6 | """Get the FPS of the work scene""" 7 | 8 | label = "Workscene FPS" 9 | order = pyblish.api.CollectorOrder 10 | hosts = ["houdini"] 11 | 12 | def process(self, context): 13 | fps = hou.fps() 14 | self.log.info("Workscene FPS: %s" % fps) 15 | context.data.update({"fps": fps}) 16 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/extract_alembic.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | 6 | 7 | class ExtractAlembic(colorbleed.api.Extractor): 8 | 9 | order = pyblish.api.ExtractorOrder 10 | label = "Extract Alembic" 11 | hosts = ["houdini"] 12 | families = ["colorbleed.pointcache", "colorbleed.camera"] 13 | 14 | def process(self, instance): 15 | 16 | import hou 17 | 18 | ropnode = instance[0] 19 | 20 | # Get the filename from the filename parameter 21 | output = ropnode.evalParm("filename") 22 | staging_dir = os.path.dirname(output) 23 | instance.data["stagingDir"] = staging_dir 24 | 25 | file_name = os.path.basename(output) 26 | 27 | # We run the render 28 | self.log.info("Writing alembic '%s' to '%s'" % (file_name, 29 | staging_dir)) 30 | try: 31 | ropnode.render() 32 | except hou.Error as exc: 33 | # The hou.Error is not inherited from a Python Exception class, 34 | # so we explicitly capture the houdini error, otherwise pyblish 35 | # will remain hanging. 36 | import traceback 37 | traceback.print_exc() 38 | raise RuntimeError("Render failed: {0}".format(exc)) 39 | 40 | if "files" not in instance.data: 41 | instance.data["files"] = [] 42 | 43 | instance.data["files"].append(file_name) 44 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/extract_vdb_cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | 6 | 7 | class ExtractVDBCache(colorbleed.api.Extractor): 8 | 9 | order = pyblish.api.ExtractorOrder + 0.1 10 | label = "Extract VDB Cache" 11 | families = ["colorbleed.vdbcache"] 12 | hosts = ["houdini"] 13 | 14 | def process(self, instance): 15 | 16 | import hou 17 | 18 | ropnode = instance[0] 19 | 20 | # Get the filename from the filename parameter 21 | # `.evalParm(parameter)` will make sure all tokens are resolved 22 | sop_output = ropnode.evalParm("sopoutput") 23 | staging_dir = os.path.normpath(os.path.dirname(sop_output)) 24 | instance.data["stagingDir"] = staging_dir 25 | file_name = os.path.basename(sop_output) 26 | 27 | self.log.info("Writing VDB '%s' to '%s'" % (file_name, staging_dir)) 28 | try: 29 | ropnode.render() 30 | except hou.Error as exc: 31 | # The hou.Error is not inherited from a Python Exception class, 32 | # so we explicitly capture the houdini error, otherwise pyblish 33 | # will remain hanging. 34 | import traceback 35 | traceback.print_exc() 36 | raise RuntimeError("Render failed: {0}".format(exc)) 37 | 38 | if "files" not in instance.data: 39 | instance.data["files"] = [] 40 | 41 | output = instance.data["frames"] 42 | 43 | instance.data["files"].append(output) 44 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_alembic_input_node.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateAlembicInputNode(pyblish.api.InstancePlugin): 6 | """Validate that the node connected to the output is correct 7 | 8 | The connected node cannot be of the following types for Alembic: 9 | - VDB 10 | - Volume 11 | 12 | """ 13 | 14 | order = colorbleed.api.ValidateContentsOrder + 0.1 15 | families = ["colorbleed.pointcache"] 16 | hosts = ["houdini"] 17 | label = "Validate Input Node (Abc)" 18 | 19 | def process(self, instance): 20 | invalid = self.get_invalid(instance) 21 | if invalid: 22 | raise RuntimeError("Primitive types found that are not supported" 23 | "for Alembic output.") 24 | 25 | @classmethod 26 | def get_invalid(cls, instance): 27 | 28 | invalid_prim_types = ["VDB", "Volume"] 29 | node = instance.data["output_node"] 30 | 31 | geo = node.geometry() 32 | invalid = False 33 | for prim_type in invalid_prim_types: 34 | if geo.countPrimType(prim_type) > 0: 35 | cls.log.error("Found a primitive which is of type '%s' !" 36 | % prim_type) 37 | invalid = True 38 | 39 | if invalid: 40 | return [instance] 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_bypass.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateBypassed(pyblish.api.InstancePlugin): 6 | """Validate all primitives build hierarchy from attribute when enabled. 7 | 8 | The name of the attribute must exist on the prims and have the same name 9 | as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic 10 | ROP node whenever Build Hierarchy from Attribute is enabled. 11 | 12 | """ 13 | 14 | order = colorbleed.api.ValidateContentsOrder - 0.1 15 | families = ["*"] 16 | hosts = ["houdini"] 17 | label = "Validate ROP Bypass" 18 | 19 | def process(self, instance): 20 | 21 | invalid = self.get_invalid(instance) 22 | if invalid: 23 | rop = invalid[0] 24 | raise RuntimeError( 25 | "ROP node %s is set to bypass, publishing cannot continue.." % 26 | rop.path() 27 | ) 28 | 29 | @classmethod 30 | def get_invalid(cls, instance): 31 | 32 | rop = instance[0] 33 | if rop.isBypassed(): 34 | return [rop] 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_camera_rop.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateCameraROP(pyblish.api.InstancePlugin): 6 | """Validate Camera ROP settings.""" 7 | 8 | order = colorbleed.api.ValidateContentsOrder 9 | families = ['colorbleed.camera'] 10 | hosts = ['houdini'] 11 | label = 'Camera ROP' 12 | 13 | def process(self, instance): 14 | 15 | import hou 16 | 17 | node = instance[0] 18 | if node.parm("use_sop_path").eval(): 19 | raise RuntimeError("Alembic ROP for Camera export should not be " 20 | "set to 'Use Sop Path'. Please disable.") 21 | 22 | # Get the root and objects parameter of the Alembic ROP node 23 | root = node.parm("root").eval() 24 | objects = node.parm("objects").eval() 25 | assert root, "Root parameter must be set on Alembic ROP" 26 | assert root.startswith("/"), "Root parameter must start with slash /" 27 | assert objects, "Objects parameter must be set on Alembic ROP" 28 | assert len(objects.split(" ")) == 1, "Must have only a single object." 29 | 30 | # Check if the object exists and is a camera 31 | path = root + "/" + objects 32 | camera = hou.node(path) 33 | 34 | if not camera: 35 | raise ValueError("Camera path does not exist: %s" % path) 36 | 37 | if not camera.type().name() == "cam": 38 | raise ValueError("Object set in Alembic ROP is not a camera: " 39 | "%s (type: %s)" % (camera, camera.type().name())) 40 | 41 | 42 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_file_extension.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyblish.api 3 | 4 | from colorbleed.houdini import lib 5 | 6 | 7 | class ValidateFileExtension(pyblish.api.InstancePlugin): 8 | """Validate the output file extension fits the output family. 9 | 10 | File extensions: 11 | - Pointcache must be .abc 12 | - Camera must be .abc 13 | - VDB must be .vdb 14 | 15 | """ 16 | 17 | order = pyblish.api.ValidatorOrder 18 | families = ["colorbleed.pointcache", 19 | "colorbleed.camera", 20 | "colorbleed.vdbcache"] 21 | hosts = ["houdini"] 22 | label = "Output File Extension" 23 | 24 | family_extensions = { 25 | "colorbleed.pointcache": ".abc", 26 | "colorbleed.camera": ".abc", 27 | "colorbleed.vdbcache": ".vdb" 28 | } 29 | 30 | def process(self, instance): 31 | 32 | invalid = self.get_invalid(instance) 33 | if invalid: 34 | raise RuntimeError("ROP node has incorrect " 35 | "file extension: %s" % invalid) 36 | 37 | @classmethod 38 | def get_invalid(cls, instance): 39 | 40 | import hou 41 | 42 | # Get ROP node from instance 43 | node = instance[0] 44 | 45 | # Create lookup for current family in instance 46 | families = instance.data.get("families", list()) 47 | family = instance.data.get("family", None) 48 | if family: 49 | families.append(family) 50 | families = set(families) 51 | 52 | # Perform extension check 53 | output = lib.get_output_parameter(node).eval() 54 | _, output_extension = os.path.splitext(output) 55 | 56 | for family in families: 57 | extension = cls.family_extensions.get(family, None) 58 | if extension is None: 59 | raise RuntimeError("Unsupported family: %s" % family) 60 | 61 | if output_extension != extension: 62 | return [node.path()] 63 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_frame_token.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | from colorbleed.houdini import lib 4 | 5 | 6 | class ValidateFrameToken(pyblish.api.InstancePlugin): 7 | """Validate if the unexpanded string contains the frame ('$F') token 8 | 9 | This validator will *only* check the output parameter of the node if 10 | the Valid Frame Range is not set to 'Render Current Frame' 11 | 12 | Rules: 13 | If you render out a frame range it is mandatory to have the 14 | frame token - '$F4' or similar - to ensure that each frame gets 15 | written. If this is not the case you will override the same file 16 | every time a frame is written out. 17 | 18 | Examples: 19 | Good: 'my_vbd_cache.$F4.vdb' 20 | Bad: 'my_vbd_cache.vdb' 21 | 22 | """ 23 | 24 | order = pyblish.api.ValidatorOrder 25 | label = "Validate Frame Token" 26 | families = ["colorbleed.vdbcache"] 27 | 28 | def process(self, instance): 29 | 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Output settings do no match for '%s'" % 33 | instance) 34 | 35 | @classmethod 36 | def get_invalid(cls, instance): 37 | 38 | node = instance[0] 39 | 40 | # Check trange parm, 0 means Render Current Frame 41 | frame_range = node.evalParm("trange") 42 | if frame_range == 0: 43 | return [] 44 | 45 | output_parm = lib.get_output_parameter(node) 46 | unexpanded_str = output_parm.unexpandedString() 47 | 48 | if "$F" not in unexpanded_str: 49 | cls.log.error("No frame token found in '%s'" % node.path()) 50 | return [instance] 51 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): 6 | """Validate Create Intermediate Directories is enabled on ROP node.""" 7 | 8 | order = colorbleed.api.ValidateContentsOrder 9 | families = ['colorbleed.pointcache', 10 | 'colorbleed.camera', 11 | 'colorbleed.vdbcache'] 12 | hosts = ['houdini'] 13 | label = 'Create Intermediate Directories Checked' 14 | 15 | def process(self, instance): 16 | 17 | invalid = self.get_invalid(instance) 18 | if invalid: 19 | raise RuntimeError("Found ROP node with Create Intermediate " 20 | "Directories turned off: %s" % invalid) 21 | 22 | @classmethod 23 | def get_invalid(cls, instance): 24 | 25 | result = [] 26 | 27 | for node in instance[:]: 28 | if node.parm("mkpath").eval() != 1: 29 | cls.log.error("Invalid settings found on `%s`" % node.path()) 30 | result.append(node.path()) 31 | 32 | return result 33 | 34 | 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/houdini/publish/validate_vdb_input_node.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateVDBInputNode(pyblish.api.InstancePlugin): 6 | """Validate that the node connected to the output node is of type VDB 7 | 8 | Regardless of the amount of VDBs create the output will need to have an 9 | equal amount of VDBs, points, primitives and vertices 10 | 11 | A VDB is an inherited type of Prim, holds the following data: 12 | - Primitives: 1 13 | - Points: 1 14 | - Vertices: 1 15 | - VDBs: 1 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateContentsOrder + 0.1 20 | families = ["colorbleed.vdbcache"] 21 | hosts = ["houdini"] 22 | label = "Validate Input Node (VDB)" 23 | 24 | def process(self, instance): 25 | invalid = self.get_invalid(instance) 26 | if invalid: 27 | raise RuntimeError("Node connected to the output node is not" 28 | "of type VDB!") 29 | 30 | @classmethod 31 | def get_invalid(cls, instance): 32 | 33 | node = instance.data["output_node"] 34 | if node is None: 35 | cls.log.error("SOP path is not correctly set on " 36 | "ROP node '%s'." % instance[0].path()) 37 | return [instance] 38 | 39 | prims = node.geometry().prims() 40 | nr_of_prims = len(prims) 41 | 42 | nr_of_points = len(node.geometry().points()) 43 | if nr_of_points != nr_of_prims: 44 | cls.log.error("The number of primitives and points do not match") 45 | return [instance] 46 | 47 | for prim in prims: 48 | if prim.numVertices() != 1: 49 | cls.log.error("Found primitive with more than 1 vertex!") 50 | return [instance] 51 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_animation.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreateAnimation(avalon.maya.Creator): 6 | """Animation output for character rigs""" 7 | 8 | label = "Animation" 9 | family = "colorbleed.animation" 10 | icon = "male" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreateAnimation, self).__init__(*args, **kwargs) 14 | 15 | # create an ordered dict with the existing data first 16 | 17 | # get basic animation data : start / end / handles / steps 18 | for key, value in lib.collect_animation_data().items(): 19 | self.data[key] = value 20 | 21 | # Write vertex colors with the geometry. 22 | self.data["writeColorSets"] = False 23 | 24 | # Include only renderable visible shapes. 25 | # Skips locators and empty transforms 26 | self.data["renderableOnly"] = False 27 | 28 | # Include only nodes that are visible at least once during the 29 | # frame range. 30 | self.data["visibleOnly"] = False 31 | 32 | # Include the groups above the out_SET content 33 | self.data["includeParentHierarchy"] = False # Include parent groups 34 | 35 | # Default to exporting world-space 36 | self.data["worldSpace"] = True 37 | 38 | # Apply Euler filter to rotations 39 | self.data["eulerFilter"] = True 40 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_camera.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreateCamera(avalon.maya.Creator): 6 | """Single baked camera""" 7 | 8 | label = "Camera" 9 | family = "colorbleed.camera" 10 | icon = "video-camera" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreateCamera, self).__init__(*args, **kwargs) 14 | 15 | # get basic animation data : start / end / handles / steps 16 | animation_data = lib.collect_animation_data() 17 | for key, value in animation_data.items(): 18 | self.data[key] = value 19 | 20 | # Bake to world space by default, when this is False it will also 21 | # include the parent hierarchy in the baked results 22 | self.data['bakeToWorldSpace'] = True 23 | 24 | # Apply Euler filter to rotations for alembic 25 | self.data["eulerFilter"] = True 26 | 27 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_fbx.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreateFBX(avalon.maya.Creator): 6 | """FBX Export""" 7 | 8 | label = "FBX" 9 | family = "colorbleed.fbx" 10 | icon = "plug" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreateFBX, self).__init__(*args, **kwargs) 14 | 15 | # get basic animation data : start / end / handles / steps 16 | for key, value in lib.collect_animation_data().items(): 17 | self.data[key] = value 18 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_look.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreateLook(avalon.maya.Creator): 6 | """Shader connections defining shape look""" 7 | 8 | label = "Look" 9 | family = "colorbleed.look" 10 | icon = "paint-brush" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreateLook, self).__init__(*args, **kwargs) 14 | 15 | self.data["renderlayer"] = lib.get_current_renderlayer() 16 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_mayaascii.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | 3 | 4 | class CreateMayaAscii(avalon.maya.Creator): 5 | """Raw Maya Ascii file export""" 6 | 7 | label = "Maya Ascii" 8 | family = "colorbleed.mayaAscii" 9 | icon = "file-archive-o" 10 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_model.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | 3 | 4 | class CreateModel(avalon.maya.Creator): 5 | """Polygonal static geometry""" 6 | 7 | label = "Model" 8 | family = "colorbleed.model" 9 | icon = "cube" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreateModel, self).__init__(*args, **kwargs) 13 | 14 | # Vertex colors with the geometry 15 | self.data["writeColorSets"] = False 16 | 17 | # Include attributes by attribute name or prefix 18 | self.data["attr"] = "" 19 | self.data["attrPrefix"] = "" 20 | 21 | # Whether to include parent hierarchy of nodes in the instance 22 | self.data["includeParentHierarchy"] = False 23 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_pointcache.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreatePointCache(avalon.maya.Creator): 6 | """Alembic pointcache for animated data""" 7 | 8 | label = "Point Cache" 9 | family = "colorbleed.pointcache" 10 | icon = "gears" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreatePointCache, self).__init__(*args, **kwargs) 14 | 15 | # Add animation data 16 | self.data.update(lib.collect_animation_data()) 17 | 18 | self.data["writeColorSets"] = False # Vertex colors with the geometry. 19 | self.data["renderableOnly"] = False # Only renderable visible shapes 20 | self.data["visibleOnly"] = False # only nodes that are visible 21 | self.data["includeParentHierarchy"] = False # Include parent groups 22 | self.data["worldSpace"] = True # Default to exporting world-space 23 | 24 | # Add options for custom attributes 25 | self.data["attr"] = "" 26 | self.data["attrPrefix"] = "" 27 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_renderglobals.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import colorbleed.maya.lib as lib 4 | 5 | from avalon.vendor import requests 6 | import avalon.maya 7 | from avalon import api 8 | 9 | 10 | class CreateRenderGlobals(avalon.maya.Creator): 11 | """Submit Mayabatch renderlayers to Deadline""" 12 | 13 | label = "Render Globals" 14 | family = "colorbleed.renderglobals" 15 | icon = "gears" 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(CreateRenderGlobals, self).__init__(*args, **kwargs) 19 | 20 | # We won't be publishing this one 21 | self.data["id"] = "avalon.renderglobals" 22 | 23 | # Get available Deadline pools 24 | AVALON_DEADLINE = api.Session["AVALON_DEADLINE"] 25 | argument = "{}/api/pools?NamesOnly=true".format(AVALON_DEADLINE) 26 | response = requests.get(argument) 27 | if not response.ok: 28 | self.log.warning("No pools retrieved") 29 | pools = [] 30 | else: 31 | pools = response.json() 32 | 33 | # We don't need subset or asset attributes 34 | self.data.pop("subset", None) 35 | self.data.pop("asset", None) 36 | self.data.pop("active", None) 37 | 38 | self.data["suspendPublishJob"] = False 39 | self.data["extendFrames"] = False 40 | self.data["overrideExistingFrame"] = True 41 | self.data["useLegacyRenderLayers"] = True 42 | self.data["priority"] = 50 43 | self.data["framesPerTask"] = 1 44 | self.data["whitelist"] = False 45 | self.data["machineList"] = "" 46 | self.data["useMayaBatch"] = True 47 | self.data["primaryPool"] = pools 48 | # We add a string "-" to allow the user to not set any secondary pools 49 | self.data["secondaryPool"] = ["-"] + pools 50 | 51 | self.options = {"useSelection": False} # Force no content 52 | 53 | def process(self): 54 | 55 | exists = cmds.ls(self.name) 56 | assert len(exists) <= 1, ( 57 | "More than one renderglobal exists, this is a bug" 58 | ) 59 | 60 | if exists: 61 | return cmds.warning("%s already exists." % exists[0]) 62 | 63 | with lib.undo_chunk(): 64 | super(CreateRenderGlobals, self).process() 65 | cmds.setAttr("{}.machineList".format(self.name), lock=True) 66 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_rig.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import colorbleed.maya.lib as lib 4 | import avalon.maya 5 | 6 | 7 | class CreateRig(avalon.maya.Creator): 8 | """Artist-friendly rig with controls to direct motion""" 9 | 10 | label = "Rig" 11 | family = "colorbleed.rig" 12 | icon = "wheelchair" 13 | 14 | def process(self): 15 | 16 | with lib.undo_chunk(): 17 | instance = super(CreateRig, self).process() 18 | 19 | self.log.info("Creating Rig instance set up ...") 20 | controls = cmds.sets(name="controls_SET", empty=True) 21 | pointcache = cmds.sets(name="out_SET", empty=True) 22 | cmds.sets([controls, pointcache], forceElement=instance) 23 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_setdress.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | 3 | 4 | class CreateSetDress(avalon.maya.Creator): 5 | """A grouped package of loaded content""" 6 | 7 | label = "Set Dress" 8 | family = "colorbleed.setdress" 9 | icon = "cubes" 10 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_vrayproxy.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | 3 | 4 | class CreateVrayProxy(avalon.maya.Creator): 5 | """Export a VRayMesh Proxy for meshes""" 6 | 7 | label = "VRay Proxy" 8 | family = "colorbleed.vrayproxy" 9 | icon = "gears" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreateVrayProxy, self).__init__(*args, **kwargs) 13 | 14 | self.data["animation"] = False 15 | self.data["startFrame"] = 1 16 | self.data["endFrame"] = 1 17 | 18 | # Write vertex colors 19 | self.data["vertexColors"] = False 20 | 21 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_vrayscene.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | 3 | 4 | class CreateVRayScene(avalon.maya.Creator): 5 | """Submit VRayScene to Deadline""" 6 | 7 | label = "VRay Scene" 8 | family = "colorbleed.vrayscene" 9 | icon = "cubes" 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(CreateVRayScene, self).__init__(*args, **kwargs) 13 | 14 | # We don't need subset or asset attributes 15 | self.data.pop("subset", None) 16 | self.data.pop("asset", None) 17 | self.data.pop("active", None) 18 | 19 | self.data.update({ 20 | "id": "avalon.vrayscene", # We won't be publishing this one 21 | "suspendRenderJob": False, 22 | "suspendPublishJob": False, 23 | "extendFrames": False, 24 | "pools": "", 25 | "framesPerTask": 1 26 | }) 27 | 28 | self.options = {"useSelection": False} # Force no content 29 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_yeti_cache.py: -------------------------------------------------------------------------------- 1 | import avalon.maya 2 | from colorbleed.maya import lib 3 | 4 | 5 | class CreateYetiCache(avalon.maya.Creator): 6 | """Yeti Cache output for Yeti nodes""" 7 | 8 | label = "Yeti Cache" 9 | family = "colorbleed.yeticache" 10 | icon = "pagelines" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CreateYetiCache, self).__init__(*args, **kwargs) 14 | 15 | self.data["preroll"] = 0 16 | 17 | # Add animation data without step and handles 18 | anim_data = lib.collect_animation_data() 19 | anim_data.pop("step") 20 | anim_data.pop("handles") 21 | self.data.update(anim_data) 22 | 23 | # Add samples 24 | self.data["samples"] = 3 25 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/create/colorbleed_yeti_rig.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import colorbleed.maya.lib as lib 4 | import avalon.maya 5 | 6 | 7 | class CreateYetiRig(avalon.maya.Creator): 8 | """Create a Yeti Rig""" 9 | 10 | label = "Yeti Rig" 11 | family = "colorbleed.yetiRig" 12 | icon = "usb" 13 | 14 | def process(self): 15 | 16 | with lib.undo_chunk(): 17 | instance = super(CreateYetiRig, self).process() 18 | 19 | self.log.info("Creating Rig instance set up ...") 20 | input_meshes = cmds.sets(name="input_SET", empty=True) 21 | cmds.sets(input_meshes, forceElement=instance) 22 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_alembic.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class AbcLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Reference Alembic""" 6 | 7 | families = ["colorbleed.animation", 8 | "colorbleed.pointcache"] 9 | label = "Reference animation" 10 | representations = ["abc"] 11 | order = -10 12 | icon = "code-fork" 13 | color = "orange" 14 | 15 | def process_reference(self, context, name, namespace, data): 16 | 17 | import maya.cmds as cmds 18 | 19 | cmds.loadPlugin("AbcImport.mll", quiet=True) 20 | nodes = cmds.file(self.fname, 21 | namespace=namespace, 22 | sharedReferenceFile=False, 23 | groupReference=True, 24 | groupName="{}:{}".format(namespace, name), 25 | reference=True, 26 | returnNewNodes=True) 27 | 28 | self[:] = nodes 29 | 30 | return nodes 31 | 32 | def switch(self, container, representation): 33 | self.update(container, representation) 34 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_camera.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class CameraLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Reference Camera""" 6 | 7 | families = ["colorbleed.camera"] 8 | label = "Reference camera" 9 | representations = ["abc", "ma"] 10 | order = -10 11 | icon = "code-fork" 12 | color = "orange" 13 | 14 | def process_reference(self, context, name, namespace, data): 15 | 16 | import maya.cmds as cmds 17 | # Get family type from the context 18 | 19 | cmds.loadPlugin("AbcImport.mll", quiet=True) 20 | nodes = cmds.file(self.fname, 21 | namespace=namespace, 22 | sharedReferenceFile=False, 23 | groupReference=True, 24 | groupName="{}:{}".format(namespace, name), 25 | reference=True, 26 | returnNewNodes=True) 27 | 28 | cameras = cmds.ls(nodes, type="camera") 29 | 30 | # Check the Maya version, lockTransform has been introduced since 31 | # Maya 2016.5 Ext 2 32 | version = int(cmds.about(version=True)) 33 | if version >= 2016: 34 | for camera in cameras: 35 | cmds.camera(camera, edit=True, lockTransform=True) 36 | else: 37 | self.log.warning("This version of Maya does not support locking of" 38 | " transforms of cameras.") 39 | 40 | self[:] = nodes 41 | 42 | return nodes 43 | 44 | def switch(self, container, representation): 45 | self.update(container, representation) 46 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_fbx.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class FBXLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Load the FBX""" 6 | 7 | families = ["colorbleed.fbx"] 8 | representations = ["fbx"] 9 | 10 | label = "Reference FBX" 11 | order = -10 12 | icon = "code-fork" 13 | color = "orange" 14 | 15 | def process_reference(self, context, name, namespace, data): 16 | 17 | import maya.cmds as cmds 18 | from avalon import maya 19 | 20 | # Ensure FBX plug-in is loaded 21 | cmds.loadPlugin("fbxmaya", quiet=True) 22 | 23 | with maya.maintained_selection(): 24 | nodes = cmds.file(self.fname, 25 | namespace=namespace, 26 | reference=True, 27 | returnNewNodes=True, 28 | groupReference=True, 29 | groupName="{}:{}".format(namespace, name)) 30 | 31 | self[:] = nodes 32 | 33 | return nodes 34 | 35 | def switch(self, container, representation): 36 | self.update(container, representation) 37 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_look.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class LookLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Reference look .ma file without assigning shaders.""" 6 | 7 | families = ["colorbleed.look"] 8 | representations = ["ma"] 9 | 10 | label = "Reference look" 11 | order = -10 12 | icon = "code-fork" 13 | color = "orange" 14 | 15 | def process_reference(self, context, name, namespace, data): 16 | 17 | import maya.cmds as cmds 18 | from avalon import maya 19 | 20 | with maya.maintained_selection(): 21 | nodes = cmds.file(self.fname, 22 | namespace=namespace, 23 | reference=True, 24 | returnNewNodes=True) 25 | 26 | self[:] = nodes 27 | 28 | def switch(self, container, representation): 29 | self.update(container, representation) 30 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_mayaascii.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class MayaAsciiLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Load the model""" 6 | 7 | families = ["colorbleed.mayaAscii"] 8 | representations = ["ma"] 9 | 10 | label = "Reference Maya Ascii" 11 | order = -10 12 | icon = "code-fork" 13 | color = "orange" 14 | 15 | def process_reference(self, context, name, namespace, data): 16 | 17 | import maya.cmds as cmds 18 | from avalon import maya 19 | 20 | with maya.maintained_selection(): 21 | nodes = cmds.file(self.fname, 22 | namespace=namespace, 23 | reference=True, 24 | returnNewNodes=True, 25 | groupReference=True, 26 | groupName="{}:{}".format(namespace, name)) 27 | 28 | self[:] = nodes 29 | 30 | return nodes 31 | 32 | def switch(self, container, representation): 33 | self.update(container, representation) 34 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_rig.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import colorbleed.maya.plugin 4 | from avalon import api, maya 5 | 6 | 7 | class RigLoader(colorbleed.maya.plugin.ReferenceLoader): 8 | """Specific loader for rigs 9 | 10 | This automatically creates an animation publish instance upon load. 11 | 12 | """ 13 | 14 | families = ["colorbleed.rig"] 15 | representations = ["ma"] 16 | 17 | label = "Reference rig" 18 | order = -10 19 | icon = "code-fork" 20 | color = "orange" 21 | 22 | def process_reference(self, context, name, namespace, data): 23 | 24 | nodes = cmds.file(self.fname, 25 | namespace=namespace, 26 | reference=True, 27 | returnNewNodes=True, 28 | groupReference=True, 29 | groupName="{}:{}".format(namespace, name)) 30 | 31 | # Store for post-process 32 | self[:] = nodes 33 | if data.get("post_process", True): 34 | self._post_process(name, namespace, context, data) 35 | 36 | return nodes 37 | 38 | def _post_process(self, name, namespace, context, data): 39 | 40 | # TODO(marcus): We are hardcoding the name "out_SET" here. 41 | # Better register this keyword, so that it can be used 42 | # elsewhere, such as in the Integrator plug-in, 43 | # without duplication. 44 | 45 | output = next((node for node in self if 46 | node.endswith("out_SET")), None) 47 | controls = next((node for node in self if 48 | node.endswith("controls_SET")), None) 49 | 50 | assert output, "No out_SET in rig, this is a bug." 51 | assert controls, "No controls_SET in rig, this is a bug." 52 | 53 | # Find the roots amongst the loaded nodes 54 | roots = cmds.ls(self[:], assemblies=True, long=True) 55 | assert roots, "No root nodes in rig, this is a bug." 56 | 57 | asset = api.Session["AVALON_ASSET"] 58 | dependency = str(context["representation"]["_id"]) 59 | 60 | # Create the animation instance 61 | with maya.maintained_selection(): 62 | cmds.select([output, controls] + roots, noExpand=True) 63 | api.create(name=namespace, 64 | asset=asset, 65 | family="colorbleed.animation", 66 | options={"useSelection": True}, 67 | data={"dependencies": dependency}) 68 | 69 | def switch(self, container, representation): 70 | self.update(container, representation) 71 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/load/load_yeti_rig.py: -------------------------------------------------------------------------------- 1 | import colorbleed.maya.plugin 2 | 3 | 4 | class YetiRigLoader(colorbleed.maya.plugin.ReferenceLoader): 5 | """Load a Yeti Rig for simulations""" 6 | 7 | families = ["colorbleed.yetiRig"] 8 | representations = ["ma"] 9 | 10 | label = "Load Yeti Rig" 11 | order = -9 12 | icon = "code-fork" 13 | color = "orange" 14 | 15 | def process_reference(self, context, name=None, namespace=None, data=None): 16 | 17 | import maya.cmds as cmds 18 | from avalon import maya 19 | 20 | with maya.maintained_selection(): 21 | nodes = cmds.file(self.fname, 22 | namespace=namespace, 23 | reference=True, 24 | returnNewNodes=True, 25 | groupReference=True, 26 | groupName="{}:{}".format(namespace, name)) 27 | 28 | self[:] = nodes 29 | 30 | self.log.info("Yeti Rig Connection Manager will be available soon") 31 | 32 | return nodes 33 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_animation.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import maya.cmds as cmds 4 | 5 | 6 | class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): 7 | """Collect out hierarchy data for instance. 8 | 9 | Collect all hierarchy nodes which reside in the out_SET of the animation 10 | instance or point cache instance. This is to unify the logic of retrieving 11 | that specific data. This eliminates the need to write two separate pieces 12 | of logic to fetch all hierarchy nodes. 13 | 14 | Results in a list of nodes from the content of the instances 15 | 16 | """ 17 | 18 | order = pyblish.api.CollectorOrder + 0.4 19 | families = ["colorbleed.animation"] 20 | label = "Collect Animation Output Geometry" 21 | hosts = ["maya"] 22 | 23 | ignore_type = ["constraints"] 24 | 25 | def process(self, instance): 26 | """Collect the hierarchy nodes""" 27 | 28 | family = instance.data["family"] 29 | out_set = next((i for i in instance.data["setMembers"] if 30 | i.endswith("out_SET")), None) 31 | 32 | if out_set is None: 33 | warning = "Expecting out_SET for instance of family '%s'" % family 34 | self.log.warning(warning) 35 | return 36 | 37 | members = cmds.ls(cmds.sets(out_set, query=True), long=True) 38 | 39 | # Get all the relatives of the members 40 | descendants = cmds.listRelatives(members, 41 | allDescendents=True, 42 | fullPath=True) or [] 43 | descendants = cmds.ls(descendants, noIntermediate=True, long=True) 44 | 45 | # Add members and descendants together for a complete overview 46 | hierarchy = members + descendants 47 | 48 | # Ignore certain node types (e.g. constraints) 49 | ignore = cmds.ls(hierarchy, type=self.ignore_type, long=True) 50 | if ignore: 51 | ignore = set(ignore) 52 | hierarchy = [node for node in hierarchy if node not in ignore] 53 | 54 | # Store data in the instance for the validator 55 | instance.data["out_hierarchy"] = hierarchy 56 | 57 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_current_file.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | 5 | 6 | class CollectMayaCurrentFile(pyblish.api.ContextPlugin): 7 | """Inject the current working file into context""" 8 | 9 | order = pyblish.api.CollectorOrder - 0.5 10 | label = "Maya Current File" 11 | hosts = ['maya'] 12 | 13 | def process(self, context): 14 | """Inject the current working file""" 15 | current_file = cmds.file(query=True, sceneName=True) 16 | context.data['currentFile'] = current_file 17 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_history.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | 5 | 6 | class CollectMayaHistory(pyblish.api.InstancePlugin): 7 | """Collect history for instances from the Maya scene 8 | 9 | Note: 10 | This removes render layers collected in the history 11 | 12 | This is separate from Collect Instances so we can target it towards only 13 | specific family types. 14 | 15 | """ 16 | 17 | order = pyblish.api.CollectorOrder + 0.1 18 | hosts = ["maya"] 19 | label = "Maya History" 20 | families = ["colorbleed.rig"] 21 | verbose = False 22 | 23 | def process(self, instance): 24 | 25 | # Collect the history with long names 26 | history = cmds.listHistory(instance, leaf=False) or [] 27 | history = cmds.ls(history, long=True) 28 | 29 | # Remove invalid node types (like renderlayers) 30 | invalid = cmds.ls(history, type="renderLayer", long=True) 31 | if invalid: 32 | invalid = set(invalid) # optimize lookup 33 | history = [x for x in history if x not in invalid] 34 | 35 | # Combine members with history 36 | members = instance[:] + history 37 | members = list(set(members)) # ensure unique 38 | 39 | # Update the instance 40 | instance[:] = members 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_maya_units.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | import maya.mel as mel 3 | 4 | import pyblish.api 5 | 6 | 7 | class CollectMayaUnits(pyblish.api.ContextPlugin): 8 | """Collect Maya's scene units.""" 9 | 10 | label = "Maya Units" 11 | order = pyblish.api.CollectorOrder 12 | hosts = ["maya"] 13 | 14 | def process(self, context): 15 | 16 | # Get the current linear units 17 | units = cmds.currentUnit(query=True, linear=True) 18 | 19 | # Get the current angular units ('deg' or 'rad') 20 | units_angle = cmds.currentUnit(query=True, angle=True) 21 | 22 | # Get the current time units 23 | # Using the mel command is simpler than using 24 | # `cmds.currentUnit(q=1, time=1)`. Otherwise we 25 | # have to parse the returned string value to FPS 26 | fps = mel.eval('currentTimeUnitToFPS()') 27 | 28 | context.data['linearUnits'] = units 29 | context.data['angularUnits'] = units_angle 30 | context.data['fps'] = fps 31 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_maya_workspace.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish.api 4 | 5 | from maya import cmds 6 | 7 | 8 | class CollectMayaWorkspace(pyblish.api.ContextPlugin): 9 | """Inject the current workspace into context""" 10 | 11 | order = pyblish.api.CollectorOrder - 0.5 12 | label = "Maya Workspace" 13 | 14 | hosts = ['maya'] 15 | version = (0, 1, 0) 16 | 17 | def process(self, context): 18 | workspace = cmds.workspace(rootDirectory=True, query=True) 19 | if not workspace: 20 | # Project has not been set. Files will 21 | # instead end up next to the working file. 22 | workspace = cmds.workspace(dir=True, query=True) 23 | 24 | # Maya returns forward-slashes by default 25 | normalised = os.path.normpath(workspace) 26 | 27 | context.set_data('workspaceDir', value=normalised) 28 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_model.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | 5 | 6 | class CollectModelData(pyblish.api.InstancePlugin): 7 | """Collect model data 8 | 9 | Ensures always only a single frame is extracted (current frame). 10 | 11 | Note: 12 | This is a workaround so that the `colorbleed.model` family can use the 13 | same pointcache extractor implementation as animation and pointcaches. 14 | This always enforces the "current" frame to be published. 15 | 16 | """ 17 | 18 | order = pyblish.api.CollectorOrder + 0.499 19 | label = 'Collect Model Data' 20 | families = ["colorbleed.model"] 21 | 22 | def process(self, instance): 23 | # Extract only current frame (override) 24 | frame = cmds.currentTime(query=True) 25 | instance.data['startFrame'] = frame 26 | instance.data['endFrame'] = frame 27 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_renderable_camera.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | from maya import cmds 4 | 5 | from colorbleed.maya import lib 6 | 7 | 8 | class CollectRenderableCamera(pyblish.api.InstancePlugin): 9 | """Collect the renderable camera(s) for the render layer""" 10 | 11 | order = pyblish.api.CollectorOrder + 0.01 12 | label = "Collect Renderable Camera(s)" 13 | hosts = ["maya"] 14 | families = ["colorbleed.vrayscene", 15 | "colorbleed.renderlayer"] 16 | 17 | def process(self, instance): 18 | layer = instance.data["setMembers"] 19 | 20 | cameras = cmds.ls(type="camera", long=True) 21 | renderable = [c for c in cameras if 22 | lib.get_attr_in_layer("%s.renderable" % c, layer=layer)] 23 | 24 | self.log.info("Found cameras %s: %s" % (len(renderable), renderable)) 25 | 26 | instance.data["cameras"] = renderable 27 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_workscene_fps.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | from maya import mel 3 | 4 | 5 | class CollectWorksceneFPS(pyblish.api.ContextPlugin): 6 | """Get the FPS of the work scene""" 7 | 8 | label = "Workscene FPS" 9 | order = pyblish.api.CollectorOrder 10 | hosts = ["maya"] 11 | 12 | def process(self, context): 13 | fps = mel.eval('currentTimeUnitToFPS()') 14 | self.log.info("Workscene FPS: %s" % fps) 15 | context.data.update({"fps": fps}) 16 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/collect_yeti_cache.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | 5 | from colorbleed.maya import lib 6 | 7 | SETTINGS = {"renderDensity", 8 | "renderWidth", 9 | "renderLength", 10 | "increaseRenderBounds", 11 | "imageSearchPath", 12 | "cbId"} 13 | 14 | 15 | class CollectYetiCache(pyblish.api.InstancePlugin): 16 | """Collect all information of the Yeti caches 17 | 18 | The information contains the following attributes per Yeti node 19 | 20 | - "renderDensity" 21 | - "renderWidth" 22 | - "renderLength" 23 | - "increaseRenderBounds" 24 | - "imageSearchPath" 25 | 26 | Other information is the name of the transform and it's Colorbleed ID 27 | """ 28 | 29 | order = pyblish.api.CollectorOrder + 0.45 30 | label = "Collect Yeti Cache" 31 | families = ["colorbleed.yetiRig", "colorbleed.yeticache"] 32 | hosts = ["maya"] 33 | tasks = ["animation", "fx"] 34 | 35 | def process(self, instance): 36 | 37 | # Collect fur settings 38 | settings = {"nodes": []} 39 | 40 | # Get yeti nodes and their transforms 41 | yeti_shapes = cmds.ls(instance, type="pgYetiMaya") 42 | for shape in yeti_shapes: 43 | shape_data = {"transform": None, 44 | "name": shape, 45 | "cbId": lib.get_id(shape), 46 | "attrs": None} 47 | 48 | # Get specific node attributes 49 | attr_data = {} 50 | for attr in SETTINGS: 51 | current = cmds.getAttr("%s.%s" % (shape, attr)) 52 | attr_data[attr] = current 53 | 54 | # Get transform data 55 | parent = cmds.listRelatives(shape, parent=True)[0] 56 | transform_data = {"name": parent, "cbId": lib.get_id(parent)} 57 | 58 | # Store collected data 59 | shape_data["attrs"] = attr_data 60 | shape_data["transform"] = transform_data 61 | 62 | settings["nodes"].append(shape_data) 63 | 64 | instance.data["fursettings"] = settings 65 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/extract_maya_ascii_raw.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from maya import cmds 4 | 5 | import avalon.maya 6 | import colorbleed.api 7 | 8 | 9 | class ExtractMayaAsciiRaw(colorbleed.api.Extractor): 10 | """Extract as Maya Ascii (raw) 11 | 12 | This will preserve all references, construction history, etc. 13 | 14 | """ 15 | 16 | label = "Maya ASCII (Raw)" 17 | hosts = ["maya"] 18 | families = ["colorbleed.mayaAscii"] 19 | 20 | def process(self, instance): 21 | 22 | # Define extract output file path 23 | dir_path = self.staging_dir(instance) 24 | filename = "{0}.ma".format(instance.name) 25 | path = os.path.join(dir_path, filename) 26 | 27 | # Whether to include all nodes in the instance (including those from 28 | # history) or only use the exact set members 29 | members_only = instance.data.get("exactSetMembersOnly", False) 30 | if members_only: 31 | members = instance.data.get("setMembers", list()) 32 | if not members: 33 | raise RuntimeError("Can't export 'exact set members only' " 34 | "when set is empty.") 35 | else: 36 | members = instance[:] 37 | 38 | # Perform extraction 39 | self.log.info("Performing extraction..") 40 | with avalon.maya.maintained_selection(): 41 | cmds.select(members, noExpand=True) 42 | cmds.file(path, 43 | force=True, 44 | typ="mayaAscii", 45 | exportSelected=True, 46 | preserveReferences=True, 47 | constructionHistory=True, 48 | shader=True, 49 | constraints=True, 50 | expressions=True) 51 | 52 | if "files" not in instance.data: 53 | instance.data["files"] = list() 54 | 55 | instance.data["files"].append(filename) 56 | 57 | self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) 58 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/extract_rig.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from maya import cmds 4 | 5 | import avalon.maya 6 | import colorbleed.api 7 | 8 | 9 | class ExtractColorbleedRig(colorbleed.api.Extractor): 10 | """Extract rig as Maya Ascii""" 11 | 12 | label = "Extract Rig (Maya ASCII)" 13 | hosts = ["maya"] 14 | families = ["colorbleed.rig"] 15 | 16 | def process(self, instance): 17 | 18 | # Define extract output file path 19 | dir_path = self.staging_dir(instance) 20 | filename = "{0}.ma".format(instance.name) 21 | path = os.path.join(dir_path, filename) 22 | 23 | # Perform extraction 24 | self.log.info("Performing extraction..") 25 | with avalon.maya.maintained_selection(): 26 | cmds.select(instance, noExpand=True) 27 | cmds.file(path, 28 | force=True, 29 | typ="mayaAscii", 30 | exportSelected=True, 31 | preserveReferences=False, 32 | channels=True, 33 | constraints=True, 34 | expressions=True, 35 | constructionHistory=True) 36 | 37 | if "files" not in instance.data: 38 | instance.data["files"] = list() 39 | 40 | instance.data["files"].append(filename) 41 | 42 | self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) 43 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/extract_setdress.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import os 4 | 5 | import colorbleed.api 6 | from colorbleed.maya.lib import extract_alembic 7 | 8 | from maya import cmds 9 | 10 | 11 | class ExtractSetDress(colorbleed.api.Extractor): 12 | """Produce an alembic of just point positions and normals. 13 | 14 | Positions and normals are preserved, but nothing more, 15 | for plain and predictable point caches. 16 | 17 | """ 18 | 19 | label = "Extract Set Dress" 20 | hosts = ["maya"] 21 | families = ["colorbleed.setdress"] 22 | 23 | def process(self, instance): 24 | 25 | parent_dir = self.staging_dir(instance) 26 | hierarchy_filename = "{}.abc".format(instance.name) 27 | hierarchy_path = os.path.join(parent_dir, hierarchy_filename) 28 | json_filename = "{}.json".format(instance.name) 29 | json_path = os.path.join(parent_dir, json_filename) 30 | 31 | self.log.info("Dumping scene data for debugging ..") 32 | with open(json_path, "w") as filepath: 33 | json.dump(instance.data["scenedata"], filepath, ensure_ascii=False) 34 | 35 | self.log.info("Extracting point cache ..") 36 | cmds.select(instance.data["hierarchy"]) 37 | 38 | # Run basic alembic exporter 39 | extract_alembic(file=hierarchy_path, 40 | startFrame=1.0, 41 | endFrame=1.0, 42 | **{"step": 1.0, 43 | "attr": ["cbId"], 44 | "writeVisibility": True, 45 | "writeCreases": True, 46 | "uvWrite": True, 47 | "selection": True}) 48 | 49 | instance.data["files"] = [json_filename, hierarchy_filename] 50 | 51 | # Remove data 52 | instance.data.pop("scenedata", None) 53 | 54 | cmds.select(clear=True) 55 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/extract_vrayproxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import avalon.maya 4 | import colorbleed.api 5 | 6 | from maya import cmds 7 | 8 | 9 | class ExtractVRayProxy(colorbleed.api.Extractor): 10 | """Extract the content of the instance to a vrmesh file 11 | 12 | Things to pay attention to: 13 | - If animation is toggled, are the frames correct 14 | - 15 | """ 16 | 17 | label = "VRay Proxy (.vrmesh)" 18 | hosts = ["maya"] 19 | families = ["colorbleed.vrayproxy"] 20 | 21 | def process(self, instance): 22 | 23 | staging_dir = self.staging_dir(instance) 24 | file_name = "{}.vrmesh".format(instance.name) 25 | file_path = os.path.join(staging_dir, file_name) 26 | 27 | anim_on = instance.data["animation"] 28 | if not anim_on: 29 | # Remove animation information because it is not required for 30 | # non-animated subsets 31 | instance.data.pop("startFrame", None) 32 | instance.data.pop("endFrame", None) 33 | 34 | start_frame = 1 35 | end_frame = 1 36 | else: 37 | start_frame = instance.data["startFrame"] 38 | end_frame = instance.data["endFrame"] 39 | 40 | vertex_colors = instance.data.get("vertexColors", False) 41 | 42 | # Write out vrmesh file 43 | self.log.info("Writing: '%s'" % file_path) 44 | with avalon.maya.maintained_selection(): 45 | cmds.select(instance.data["setMembers"], noExpand=True) 46 | cmds.vrayCreateProxy(exportType=1, 47 | dir=staging_dir, 48 | fname=file_name, 49 | animOn=anim_on, 50 | animType=3, 51 | startFrame=start_frame, 52 | endFrame=end_frame, 53 | vertexColorsOn=vertex_colors, 54 | ignoreHiddenObjects=True, 55 | createProxyNode=False) 56 | 57 | if "files" not in instance.data: 58 | instance.data["files"] = list() 59 | 60 | instance.data["files"].append(file_name) 61 | 62 | self.log.info("Extracted instance '%s' to: %s" 63 | % (instance.name, staging_dir)) 64 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/increment_current_file_deadline.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class IncrementCurrentFileDeadline(pyblish.api.ContextPlugin): 5 | """Increment the current file. 6 | 7 | Saves the current maya scene with an increased version number. 8 | 9 | """ 10 | 11 | label = "Increment current file" 12 | order = pyblish.api.IntegratorOrder + 9.0 13 | hosts = ["maya"] 14 | families = ["colorbleed.renderlayer", 15 | "colorbleed.vrayscene"] 16 | optional = True 17 | 18 | def process(self, context): 19 | 20 | import os 21 | from maya import cmds 22 | from colorbleed.lib import version_up 23 | from colorbleed.action import get_errored_plugins_from_data 24 | 25 | errored_plugins = get_errored_plugins_from_data(context) 26 | if any(plugin.__name__ == "MayaSubmitDeadline" 27 | for plugin in errored_plugins): 28 | raise RuntimeError("Skipping incrementing current file because " 29 | "submission to deadline failed.") 30 | 31 | current_filepath = context.data["currentFile"] 32 | new_filepath = version_up(current_filepath) 33 | 34 | # Ensure the suffix is .ma because we're saving to `mayaAscii` type 35 | if not new_filepath.endswith(".ma"): 36 | self.log.warning("Refactoring scene to .ma extension") 37 | new_filepath = os.path.splitext(new_filepath)[0] + ".ma" 38 | 39 | cmds.file(rename=new_filepath) 40 | cmds.file(save=True, force=True, type="mayaAscii") 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/save_scene.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class SaveCurrentScene(pyblish.api.ContextPlugin): 5 | """Save current scene 6 | 7 | """ 8 | 9 | label = "Save current file" 10 | order = pyblish.api.IntegratorOrder - 0.49 11 | hosts = ["maya"] 12 | families = ["colorbleed.renderlayer"] 13 | 14 | def process(self, context): 15 | import maya.cmds as cmds 16 | 17 | current = cmds.file(query=True, sceneName=True) 18 | assert context.data['currentFile'] == current 19 | 20 | self.log.info("Saving current file..") 21 | cmds.file(save=True, force=True) 22 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_animation_content.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | 6 | class ValidateAnimationContent(pyblish.api.InstancePlugin): 7 | """Adheres to the content of 'animation' family 8 | 9 | - Must have collected `out_hierarchy` data. 10 | - All nodes in `out_hierarchy` must be in the instance. 11 | 12 | """ 13 | 14 | order = colorbleed.api.ValidateContentsOrder 15 | hosts = ["maya"] 16 | families = ["colorbleed.animation"] 17 | label = "Animation Content" 18 | actions = [colorbleed.maya.action.SelectInvalidAction] 19 | 20 | @classmethod 21 | def get_invalid(cls, instance): 22 | 23 | out_set = next((i for i in instance.data["setMembers"] if 24 | i.endswith("out_SET")), None) 25 | 26 | assert out_set, ("Instance '%s' has no objectSet named: `OUT_set`. " 27 | "If this instance is an unloaded reference, " 28 | "please deactivate by toggling the 'Active' attribute" 29 | % instance.name) 30 | 31 | assert 'out_hierarchy' in instance.data, "Missing `out_hierarchy` data" 32 | 33 | # All nodes in the `out_hierarchy` must be among the nodes that are 34 | # in the instance. The nodes in the instance are found from the top 35 | # group, as such this tests whether all nodes are under that top group. 36 | 37 | lookup = set(instance[:]) 38 | invalid = [node for node in instance.data['out_hierarchy'] if 39 | node not in lookup] 40 | 41 | return invalid 42 | 43 | def process(self, instance): 44 | invalid = self.get_invalid(instance) 45 | if invalid: 46 | raise RuntimeError("Animation content is invalid. See log.") 47 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_arnold_layername.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.lib as lib 6 | 7 | 8 | class ValidateArnoldLayerName(pyblish.api.InstancePlugin): 9 | """Validate preserve layer name is enabled when rendering with Arnold""" 10 | 11 | order = colorbleed.api.ValidateContentsOrder 12 | label = "Arnold Preserve Layer Name" 13 | hosts = ["maya"] 14 | families = ["colorbleed.renderlayer"] 15 | actions = [colorbleed.api.RepairAction] 16 | 17 | def process(self, instance): 18 | 19 | if instance.data.get("renderer", None) != "arnold": 20 | # If not rendering with Arnold, ignore.. 21 | return 22 | 23 | invalid = self.get_invalid(instance) 24 | if invalid: 25 | raise ValueError("Invalid render settings found for '%s'!" 26 | % instance.name) 27 | 28 | @classmethod 29 | def get_invalid(cls, instance): 30 | 31 | drivers = cmds.ls("defaultArnoldDriver", type="aiAOVDriver") 32 | assert len(drivers) == 1, "Must have one defaultArnoldDriver" 33 | 34 | driver = drivers[0] 35 | if not cmds.getAttr("{0}.preserveLayerName".format(driver)): 36 | return True 37 | 38 | @classmethod 39 | def repair(cls, instance): 40 | cmds.setAttr("defaultArnoldDriver.preserveLayerName", True) 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_camera_attributes.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateCameraAttributes(pyblish.api.InstancePlugin): 9 | """Validates Camera has no invalid attribute keys or values. 10 | 11 | The Alembic file format does not a specifc subset of attributes as such 12 | we validate that no values are set there as the output will not match the 13 | current scene. For example the preScale, film offsets and film roll. 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateContentsOrder 18 | families = ['colorbleed.camera'] 19 | hosts = ['maya'] 20 | label = 'Camera Attributes' 21 | actions = [colorbleed.maya.action.SelectInvalidAction] 22 | 23 | DEFAULTS = [ 24 | ("filmFitOffset", 0.0), 25 | ("horizontalFilmOffset", 0.0), 26 | ("verticalFilmOffset", 0.0), 27 | ("preScale", 1.0), 28 | ("filmTranslateH", 0.0), 29 | ("filmTranslateV", 0.0), 30 | ("filmRollValue", 0.0) 31 | ] 32 | 33 | @classmethod 34 | def get_invalid(cls, instance): 35 | 36 | # get cameras 37 | members = instance.data['setMembers'] 38 | shapes = cmds.ls(members, dag=True, shapes=True, long=True) 39 | cameras = cmds.ls(shapes, type='camera', long=True) 40 | 41 | invalid = set() 42 | for cam in cameras: 43 | 44 | for attr, default_value in cls.DEFAULTS: 45 | plug = "{}.{}".format(cam, attr) 46 | value = cmds.getAttr(plug) 47 | 48 | # Check if is default value 49 | if value != default_value: 50 | cls.log.warning("Invalid attribute value: {0} " 51 | "(should be: {1}))".format(plug, 52 | default_value)) 53 | invalid.add(cam) 54 | 55 | if cmds.listConnections(plug, source=True, destination=False): 56 | # TODO: Validate correctly whether value always correct 57 | cls.log.warning("%s has incoming connections, validation " 58 | "is unpredictable." % plug) 59 | 60 | return list(invalid) 61 | 62 | def process(self, instance): 63 | """Process all the nodes in the instance""" 64 | 65 | invalid = self.get_invalid(instance) 66 | 67 | if invalid: 68 | raise RuntimeError("Invalid camera attributes: %s" % invalid) 69 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_camera_contents.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateCameraContents(pyblish.api.InstancePlugin): 9 | """Validates Camera instance contents. 10 | 11 | A Camera instance may only hold a SINGLE camera's transform, nothing else. 12 | 13 | It may hold a "locator" as shape, but different shapes are down the 14 | hierarchy. 15 | 16 | """ 17 | 18 | order = colorbleed.api.ValidateContentsOrder 19 | families = ['colorbleed.camera'] 20 | hosts = ['maya'] 21 | label = 'Camera Contents' 22 | actions = [colorbleed.maya.action.SelectInvalidAction] 23 | 24 | @classmethod 25 | def get_invalid(cls, instance): 26 | 27 | # get cameras 28 | members = instance.data['setMembers'] 29 | shapes = cmds.ls(members, dag=True, shapes=True, long=True) 30 | 31 | # single camera 32 | invalid = [] 33 | cameras = cmds.ls(shapes, type='camera', long=True) 34 | if len(cameras) != 1: 35 | cls.log.warning("Camera instance must have a single camera. " 36 | "Found {0}: {1}".format(len(cameras), cameras)) 37 | invalid.extend(cameras) 38 | 39 | # We need to check this edge case because returning an extended 40 | # list when there are no actual cameras results in 41 | # still an empty 'invalid' list 42 | if len(cameras) < 1: 43 | raise RuntimeError("No cameras in instance.") 44 | 45 | # non-camera shapes 46 | valid_shapes = cmds.ls(shapes, type=('camera', 'locator'), long=True) 47 | shapes = set(shapes) - set(valid_shapes) 48 | if shapes: 49 | shapes = list(shapes) 50 | cls.log.warning("Camera instance should only contain camera " 51 | "shapes. Found: {0}".format(shapes)) 52 | invalid.extend(shapes) 53 | 54 | invalid = list(set(invalid)) 55 | 56 | return invalid 57 | 58 | def process(self, instance): 59 | """Process all the nodes in the instance""" 60 | 61 | invalid = self.get_invalid(instance) 62 | if invalid: 63 | raise RuntimeError("Invalid camera contents: " 64 | "{0}".format(invalid)) 65 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_current_renderlayer_renderable.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | from maya import cmds 4 | from colorbleed.plugin import contextplugin_should_run 5 | 6 | 7 | class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin): 8 | """Validate if current render layer has a renderable camera 9 | 10 | There is a bug in Redshift which occurs when the current render layer 11 | at file open has no renderable camera. The error raised is as follows: 12 | 13 | "No renderable cameras found. Aborting render" 14 | 15 | This error is raised even if that render layer will not be rendered. 16 | 17 | """ 18 | 19 | label = "Current Render Layer Has Renderable Camera" 20 | order = pyblish.api.ValidatorOrder 21 | hosts = ["maya"] 22 | families = ["colorbleed.renderlayer"] 23 | 24 | def process(self, context): 25 | 26 | # Workaround bug pyblish-base#250 27 | if not contextplugin_should_run(self, context): 28 | return 29 | 30 | layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) 31 | cameras = cmds.ls(type="camera", long=True) 32 | renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) 33 | assert renderable, ("Current render layer '%s' has no renderable " 34 | "camera" % layer) 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_deadline_connection.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import avalon.api as api 4 | from avalon.vendor import requests 5 | from colorbleed.plugin import contextplugin_should_run 6 | 7 | 8 | class ValidateDeadlineConnection(pyblish.api.ContextPlugin): 9 | """Validate Deadline Web Service is running""" 10 | 11 | label = "Validate Deadline Web Service" 12 | order = pyblish.api.ValidatorOrder 13 | hosts = ["maya"] 14 | families = ["colorbleed.renderlayer"] 15 | 16 | def process(self, context): 17 | 18 | # Workaround bug pyblish-base#250 19 | if not contextplugin_should_run(self, context): 20 | return 21 | 22 | AVALON_DEADLINE = api.Session.get("AVALON_DEADLINE", 23 | "http://localhost:8082") 24 | 25 | assert AVALON_DEADLINE is not None, "Requires AVALON_DEADLINE" 26 | 27 | # Check response 28 | response = requests.get(AVALON_DEADLINE) 29 | assert response.ok, "Response must be ok" 30 | assert response.text.startswith("Deadline Web Service "), ( 31 | "Web service did not respond with 'Deadline Web Service'" 32 | ) 33 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_frame_range.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateFrameRange(pyblish.api.InstancePlugin): 6 | """Valides the frame ranges. 7 | 8 | Checks the `startFrame`, `endFrame` and `handles` data. 9 | This does NOT ensure there's actual data present. 10 | 11 | This validates: 12 | - `startFrame` is lower than or equal to the `endFrame`. 13 | - must have both the `startFrame` and `endFrame` data. 14 | - The `handles` value is not lower than zero. 15 | 16 | """ 17 | 18 | label = "Validate Frame Range" 19 | order = colorbleed.api.ValidateContentsOrder 20 | families = ["colorbleed.animation", 21 | "colorbleed.pointcache", 22 | "colorbleed.camera", 23 | "colorbleed.renderlayer", 24 | "oolorbleed.vrayproxy"] 25 | 26 | def process(self, instance): 27 | 28 | start = instance.data.get("startFrame", None) 29 | end = instance.data.get("endFrame", None) 30 | handles = instance.data.get("handles", None) 31 | 32 | # Check if any of the values are present 33 | if any(value is None for value in [start, end]): 34 | raise ValueError("No time values for this instance. " 35 | "(Missing `startFrame` or `endFrame`)") 36 | 37 | self.log.info("Comparing start %s and end %s" % (start, end)) 38 | if start > end: 39 | raise RuntimeError("The start frame is a higher value " 40 | "than the end frame: " 41 | "{0}>{1}".format(start, end)) 42 | 43 | if handles is not None: 44 | if handles < 0.0: 45 | raise RuntimeError("Handles are set to a negative value") 46 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_instance_has_members.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | 6 | class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): 7 | """Validates instance objectSet has *any* members.""" 8 | 9 | order = colorbleed.api.ValidateContentsOrder 10 | hosts = ["maya"] 11 | label = 'Instance has members' 12 | actions = [colorbleed.maya.action.SelectInvalidAction] 13 | 14 | @classmethod 15 | def get_invalid(cls, instance): 16 | 17 | invalid = list() 18 | if not instance.data["setMembers"]: 19 | objectset_name = instance.data['name'] 20 | invalid.append(objectset_name) 21 | 22 | return invalid 23 | 24 | def process(self, instance): 25 | 26 | invalid = self.get_invalid(instance) 27 | if invalid: 28 | raise RuntimeError("Empty instances found: {0}".format(invalid)) 29 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_instance_subset.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import string 4 | 5 | # Allow only characters, numbers and underscore 6 | allowed = set(string.ascii_lowercase + 7 | string.ascii_uppercase + 8 | string.digits + 9 | '_') 10 | 11 | 12 | def validate_name(subset): 13 | return all(x in allowed for x in subset) 14 | 15 | 16 | class ValidateSubsetName(pyblish.api.InstancePlugin): 17 | """Validates subset name has only valid characters""" 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | families = ["*"] 21 | label = "Subset Name" 22 | 23 | def process(self, instance): 24 | 25 | subset = instance.data.get("subset", None) 26 | 27 | # Ensure subset data 28 | if subset is None: 29 | raise RuntimeError("Instance is missing subset " 30 | "name: {0}".format(subset)) 31 | 32 | if not isinstance(subset, basestring): 33 | raise TypeError("Instance subset name must be string, " 34 | "got: {0} ({1})".format(subset, type(subset))) 35 | 36 | # Ensure is not empty subset 37 | if not subset: 38 | raise ValueError("Instance subset name is " 39 | "empty: {0}".format(subset)) 40 | 41 | # Validate subset characters 42 | if not validate_name(subset): 43 | raise ValueError("Instance subset name contains invalid " 44 | "characters: {0}".format(subset)) 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_joints_hidden.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateJointsHidden(pyblish.api.InstancePlugin): 10 | """Validate all joints are hidden visually. 11 | 12 | This includes being hidden: 13 | - visibility off, 14 | - in a display layer that has visibility off, 15 | - having hidden parents or 16 | - being an intermediate object. 17 | 18 | """ 19 | 20 | order = colorbleed.api.ValidateContentsOrder 21 | hosts = ['maya'] 22 | families = ['colorbleed.rig'] 23 | category = 'rig' 24 | version = (0, 1, 0) 25 | label = "Joints Hidden" 26 | actions = [colorbleed.maya.action.SelectInvalidAction, 27 | colorbleed.api.RepairAction] 28 | 29 | @staticmethod 30 | def get_invalid(instance): 31 | joints = cmds.ls(instance, type='joint', long=True) 32 | return [j for j in joints if lib.is_visible(j, displayLayer=True)] 33 | 34 | def process(self, instance): 35 | """Process all the nodes in the instance 'objectSet'""" 36 | invalid = self.get_invalid(instance) 37 | 38 | if invalid: 39 | raise ValueError("Visible joints found: {0}".format(invalid)) 40 | 41 | @classmethod 42 | def repair(cls, instance): 43 | import maya.mel as mel 44 | mel.eval("HideJoints") 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_look_default_shaders_connections.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | 6 | 7 | class ValidateLookDefaultShadersConnections(pyblish.api.InstancePlugin): 8 | """Validate default shaders in the scene have their default connections. 9 | 10 | For example the lambert1 could potentially be disconnected from the 11 | initialShadingGroup. As such it's not lambert1 that will be identified 12 | as the default shader which can have unpredictable results. 13 | 14 | To fix the default connections need to be made again. See the logs for 15 | more details on which connections are missing. 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | families = ['colorbleed.look'] 21 | hosts = ['maya'] 22 | label = 'Look Default Shader Connections' 23 | 24 | # The default connections to check 25 | DEFAULTS = [("initialShadingGroup.surfaceShader", "lambert1"), 26 | ("initialParticleSE.surfaceShader", "lambert1"), 27 | ("initialParticleSE.volumeShader", "particleCloud1") 28 | ] 29 | 30 | def process(self, instance): 31 | 32 | # Ensure check is run only once. We don't use ContextPlugin because 33 | # of a bug where the ContextPlugin will always be visible. Even when 34 | # the family is not present in an instance. 35 | key = "__validate_look_default_shaders_connections_checked" 36 | context = instance.context 37 | is_run = context.data.get(key, False) 38 | if is_run: 39 | return 40 | else: 41 | context.data[key] = True 42 | 43 | # Process as usual 44 | invalid = list() 45 | for plug, input_node in self.DEFAULTS: 46 | inputs = cmds.listConnections(plug, 47 | source=True, 48 | destination=False) or None 49 | 50 | if not inputs or inputs[0] != input_node: 51 | self.log.error("{0} is not connected to {1}. " 52 | "This can result in unexpected behavior. " 53 | "Please reconnect to continue.".format( 54 | plug, 55 | input_node)) 56 | invalid.append(plug) 57 | 58 | if invalid: 59 | raise RuntimeError("Invalid connections.") 60 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_look_no_default_shaders.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): 9 | """Validate if any node has a connection to a default shader. 10 | 11 | This checks whether the look has any members of: 12 | - lambert1 13 | - initialShadingGroup 14 | - initialParticleSE 15 | - particleCloud1 16 | 17 | If any of those is present it will raise an error. A look is not allowed 18 | to have any of the "default" shaders present in a scene as they can 19 | introduce problems when referenced (overriding local scene shaders). 20 | 21 | To fix this no shape nodes in the look must have any of default shaders 22 | applied. 23 | 24 | """ 25 | 26 | order = colorbleed.api.ValidateContentsOrder + 0.01 27 | families = ['colorbleed.look'] 28 | hosts = ['maya'] 29 | label = 'Look No Default Shaders' 30 | actions = [colorbleed.maya.action.SelectInvalidAction] 31 | 32 | DEFAULT_SHADERS = {"lambert1", "initialShadingGroup", 33 | "initialParticleSE", "particleCloud1"} 34 | 35 | def process(self, instance): 36 | """Process all the nodes in the instance""" 37 | 38 | invalid = self.get_invalid(instance) 39 | if invalid: 40 | raise RuntimeError("Invalid node relationships found: " 41 | "{0}".format(invalid)) 42 | 43 | @classmethod 44 | def get_invalid(cls, instance): 45 | 46 | invalid = set() 47 | for node in instance: 48 | # Get shading engine connections 49 | shaders = cmds.listConnections(node, type="shadingEngine") or [] 50 | 51 | # Check for any disallowed connections on *all* nodes 52 | if any(s in cls.DEFAULT_SHADERS for s in shaders): 53 | 54 | # Explicitly log each individual "wrong" connection. 55 | for s in shaders: 56 | if s in cls.DEFAULT_SHADERS: 57 | cls.log.error("Node has unallowed connection to " 58 | "'{}': {}".format(s, node)) 59 | 60 | invalid.add(node) 61 | 62 | return list(invalid) 63 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_look_single_shader.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateSingleShader(pyblish.api.InstancePlugin): 9 | """Validate all nurbsSurfaces and meshes have exactly one shader assigned. 10 | 11 | This will error if a shape has no shaders or more than one shader. 12 | 13 | """ 14 | 15 | order = colorbleed.api.ValidateContentsOrder 16 | families = ['colorbleed.look'] 17 | hosts = ['maya'] 18 | label = 'Look Single Shader Per Shape' 19 | actions = [colorbleed.maya.action.SelectInvalidAction] 20 | 21 | # The default connections to check 22 | def process(self, instance): 23 | 24 | invalid = self.get_invalid(instance) 25 | if invalid: 26 | raise RuntimeError("Found shapes which don't have a single shader " 27 | "assigned: " 28 | "\n{}".format(invalid)) 29 | 30 | @classmethod 31 | def get_invalid(cls, instance): 32 | 33 | # Get all shapes from the instance 34 | shapes = cmds.ls(instance, type=["nurbsSurface", "mesh"], long=True) 35 | 36 | # Check the number of connected shadingEngines per shape 37 | no_shaders = [] 38 | more_than_one_shaders = [] 39 | for shape in shapes: 40 | shading_engines = cmds.listConnections(shape, 41 | destination=True, 42 | type="shadingEngine") or [] 43 | if not shading_engines: 44 | no_shaders.append(shape) 45 | elif len(shading_engines) > 1: 46 | more_than_one_shaders.append(shape) 47 | 48 | if no_shaders: 49 | cls.log.error("No shaders found on: {}".format(no_shaders)) 50 | if more_than_one_shaders: 51 | cls.log.error("More than one shader found on: " 52 | "{}".format(more_than_one_shaders)) 53 | 54 | return no_shaders + more_than_one_shaders 55 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_maya_units.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | from colorbleed import lib 6 | import colorbleed.maya.lib as mayalib 7 | 8 | 9 | class ValidateMayaUnits(pyblish.api.ContextPlugin): 10 | """Check if the Maya units are set correct""" 11 | 12 | order = colorbleed.api.ValidateSceneOrder 13 | label = "Maya Units" 14 | hosts = ['maya'] 15 | actions = [colorbleed.api.RepairContextAction] 16 | 17 | def process(self, context): 18 | 19 | # Collected units 20 | linearunits = context.data('linearUnits') 21 | angularunits = context.data('angularUnits') 22 | fps = context.data['fps'] 23 | 24 | asset_fps = lib.get_asset_fps() 25 | 26 | self.log.info('Units (linear): {0}'.format(linearunits)) 27 | self.log.info('Units (angular): {0}'.format(angularunits)) 28 | self.log.info('Units (time): {0} FPS'.format(fps)) 29 | 30 | # Check if units are correct 31 | assert linearunits and linearunits == 'cm', ("Scene linear units must " 32 | "be centimeters") 33 | 34 | assert angularunits and angularunits == 'deg', ("Scene angular units " 35 | "must be degrees") 36 | assert fps and fps == asset_fps, "Scene must be %s FPS" % asset_fps 37 | 38 | @classmethod 39 | def repair(cls, context): 40 | """Fix the current FPS setting of the scene, set to PAL(25.0 fps)""" 41 | 42 | cls.log.info("Setting angular unit to 'degrees'") 43 | cmds.currentUnit(angle="degree") 44 | current_angle = cmds.currentUnit(query=True, angle=True) 45 | cls.log.debug(current_angle) 46 | 47 | cls.log.info("Setting linear unit to 'centimeter'") 48 | cmds.currentUnit(linear="centimeter") 49 | current_linear = cmds.currentUnit(query=True, linear=True) 50 | cls.log.debug(current_linear) 51 | 52 | cls.log.info("Setting time unit to match project") 53 | asset_fps = lib.get_asset_fps() 54 | mayalib.set_scene_fps(asset_fps) 55 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_lamina_faces.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): 9 | """Validate meshes don't have lamina faces. 10 | 11 | Lamina faces share all of their edges. 12 | 13 | """ 14 | 15 | order = colorbleed.api.ValidateMeshOrder 16 | hosts = ['maya'] 17 | families = ['colorbleed.model'] 18 | category = 'geometry' 19 | version = (0, 1, 0) 20 | label = 'Mesh Lamina Faces' 21 | actions = [colorbleed.maya.action.SelectInvalidAction] 22 | 23 | @staticmethod 24 | def get_invalid(instance): 25 | meshes = cmds.ls(instance, type='mesh', long=True) 26 | invalid = [mesh for mesh in meshes if 27 | cmds.polyInfo(mesh, laminaFaces=True)] 28 | 29 | return invalid 30 | 31 | def process(self, instance): 32 | """Process all the nodes in the instance 'objectSet'""" 33 | 34 | invalid = self.get_invalid(instance) 35 | 36 | if invalid: 37 | raise ValueError("Meshes found with lamina faces: " 38 | "{0}".format(invalid)) 39 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_no_negative_scale.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateMeshNoNegativeScale(pyblish.api.Validator): 9 | """Ensure that meshes don't have a negative scale. 10 | 11 | Using negatively scaled proxies in a VRayMesh results in inverted 12 | normals. As such we want to avoid this. 13 | 14 | We also avoid this on the rig or model because these are often the 15 | previous steps for those that are cached to proxies so we can catch this 16 | issue early. 17 | 18 | """ 19 | 20 | order = colorbleed.api.ValidateMeshOrder 21 | hosts = ['maya'] 22 | families = ['colorbleed.model'] 23 | label = 'Mesh No Negative Scale' 24 | actions = [colorbleed.maya.action.SelectInvalidAction] 25 | 26 | @staticmethod 27 | def get_invalid(instance): 28 | meshes = cmds.ls(instance, 29 | type='mesh', 30 | long=True, 31 | noIntermediate=True) 32 | 33 | invalid = [] 34 | for mesh in meshes: 35 | transform = cmds.listRelatives(mesh, parent=True, fullPath=True)[0] 36 | scale = cmds.getAttr("{0}.scale".format(transform))[0] 37 | 38 | if any(x < 0 for x in scale): 39 | invalid.append(mesh) 40 | 41 | return invalid 42 | 43 | def process(self, instance): 44 | """Process all the nodes in the instance 'objectSet'""" 45 | 46 | invalid = self.get_invalid(instance) 47 | 48 | if invalid: 49 | raise ValueError("Meshes found with negative " 50 | "scale: {0}".format(invalid)) 51 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_non_manifold.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateMeshNonManifold(pyblish.api.Validator): 9 | """Ensure that meshes don't have non-manifold edges or vertices 10 | 11 | To debug the problem on the meshes you can use Maya's modeling 12 | tool: "Mesh > Cleanup..." 13 | 14 | """ 15 | 16 | order = colorbleed.api.ValidateMeshOrder 17 | hosts = ['maya'] 18 | families = ['colorbleed.model'] 19 | label = 'Mesh Non-Manifold Vertices/Edges' 20 | actions = [colorbleed.maya.action.SelectInvalidAction] 21 | 22 | @staticmethod 23 | def get_invalid(instance): 24 | 25 | meshes = cmds.ls(instance, type='mesh', long=True) 26 | 27 | invalid = [] 28 | for mesh in meshes: 29 | if (cmds.polyInfo(mesh, nonManifoldVertices=True) or 30 | cmds.polyInfo(mesh, nonManifoldEdges=True)): 31 | invalid.append(mesh) 32 | 33 | return invalid 34 | 35 | def process(self, instance): 36 | """Process all the nodes in the instance 'objectSet'""" 37 | 38 | invalid = self.get_invalid(instance) 39 | 40 | if invalid: 41 | raise ValueError("Meshes found with non-manifold " 42 | "edges/vertices: {0}".format(invalid)) 43 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_non_zero_edge.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): 10 | """Validate meshes don't have edges with a zero length. 11 | 12 | Based on Maya's polyCleanup 'Edges with zero length'. 13 | 14 | Note: 15 | This can be slow for high-res meshes. 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateMeshOrder 20 | families = ['colorbleed.model'] 21 | hosts = ['maya'] 22 | category = 'geometry' 23 | version = (0, 1, 0) 24 | label = 'Mesh Edge Length Non Zero' 25 | actions = [colorbleed.maya.action.SelectInvalidAction] 26 | 27 | __tolerance = 1e-5 28 | 29 | @classmethod 30 | def get_invalid(cls, instance): 31 | """Return the invalid edges. 32 | Also see: http://help.autodesk.com/view/MAYAUL/2015/ENU/?guid=Mesh__Cleanup 33 | 34 | """ 35 | 36 | meshes = cmds.ls(instance, type='mesh', long=True) 37 | if not meshes: 38 | return list() 39 | 40 | # Get all edges 41 | edges = ['{0}.e[*]'.format(node) for node in meshes] 42 | 43 | # Filter by constraint on edge length 44 | invalid = lib.polyConstraint(edges, 45 | t=0x8000, # type=edge 46 | length=1, 47 | lengthbound=(0, cls.__tolerance)) 48 | 49 | return invalid 50 | 51 | def process(self, instance): 52 | """Process all meshes""" 53 | invalid = self.get_invalid(instance) 54 | if invalid: 55 | raise RuntimeError("Meshes found with zero " 56 | "edge length: {0}".format(invalid)) 57 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_normals_unlocked.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | import maya.api.OpenMaya as om2 3 | 4 | import pyblish.api 5 | import colorbleed.api 6 | import colorbleed.maya.action 7 | 8 | 9 | class ValidateMeshNormalsUnlocked(pyblish.api.Validator): 10 | """Validate all meshes in the instance have unlocked normals 11 | 12 | These can be unlocked manually through: 13 | Modeling > Mesh Display > Unlock Normals 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateMeshOrder 18 | hosts = ['maya'] 19 | families = ['colorbleed.model'] 20 | category = 'geometry' 21 | version = (0, 1, 0) 22 | label = 'Mesh Normals Unlocked' 23 | actions = [colorbleed.maya.action.SelectInvalidAction, 24 | colorbleed.api.RepairAction] 25 | optional = True 26 | 27 | @staticmethod 28 | def has_locked_normals(mesh): 29 | """Return whether mesh has at least one locked normal""" 30 | 31 | sel = om2.MGlobal.getSelectionListByName(mesh) 32 | node = sel.getDependNode(0) 33 | fn_mesh = om2.MFnMesh(node) 34 | _, normal_ids = fn_mesh.getNormalIds() 35 | for normal_id in normal_ids: 36 | if fn_mesh.isNormalLocked(normal_id): 37 | return True 38 | return False 39 | 40 | @classmethod 41 | def get_invalid(cls, instance): 42 | """Return the meshes with locked normals in instance""" 43 | 44 | meshes = cmds.ls(instance, type='mesh', long=True) 45 | return [mesh for mesh in meshes if cls.has_locked_normals(mesh)] 46 | 47 | def process(self, instance): 48 | """Raise invalid when any of the meshes have locked normals""" 49 | 50 | invalid = self.get_invalid(instance) 51 | 52 | if invalid: 53 | raise ValueError("Meshes found with " 54 | "locked normals: {0}".format(invalid)) 55 | 56 | @classmethod 57 | def repair(cls, instance): 58 | """Unlocks all normals on the meshes in this instance.""" 59 | invalid = cls.get_invalid(instance) 60 | for mesh in invalid: 61 | cmds.polyNormalPerVertex(mesh, unFreezeNormal=True) 62 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_mesh_single_uv_set.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin): 10 | """Warn on multiple UV sets existing for each polygon mesh. 11 | 12 | On versions prior to Maya 2017 this will force no multiple uv sets because 13 | the Alembic exports in Maya prior to 2017 don't support writing multiple 14 | UV sets. 15 | 16 | """ 17 | 18 | order = colorbleed.api.ValidateMeshOrder 19 | hosts = ['maya'] 20 | families = ['colorbleed.model', 'colorbleed.pointcache'] 21 | category = 'uv' 22 | optional = True 23 | version = (0, 1, 0) 24 | label = "Mesh Single UV Set" 25 | actions = [colorbleed.maya.action.SelectInvalidAction, 26 | colorbleed.api.RepairAction] 27 | 28 | @staticmethod 29 | def get_invalid(instance): 30 | 31 | meshes = cmds.ls(instance, type='mesh', long=True) 32 | 33 | invalid = [] 34 | for mesh in meshes: 35 | uvSets = cmds.polyUVSet(mesh, 36 | query=True, 37 | allUVSets=True) or [] 38 | 39 | # ensure unique (sometimes maya will list 'map1' twice) 40 | uvSets = set(uvSets) 41 | 42 | if len(uvSets) != 1: 43 | invalid.append(mesh) 44 | 45 | return invalid 46 | 47 | def process(self, instance): 48 | """Process all the nodes in the instance 'objectSet'""" 49 | 50 | invalid = self.get_invalid(instance) 51 | 52 | if invalid: 53 | 54 | message = "Nodes found with multiple UV sets: {0}".format(invalid) 55 | 56 | # Maya 2017 and up allows multiple UV sets in Alembic exports 57 | # so we allow it, yet just warn the user to ensure they know about 58 | # the other UV sets. 59 | allowed = int(cmds.about(version=True)) >= 2017 60 | 61 | if allowed: 62 | self.log.warning(message) 63 | else: 64 | raise ValueError(message) 65 | 66 | @classmethod 67 | def repair(cls, instance): 68 | for mesh in cls.get_invalid(instance): 69 | lib.remove_other_uv_sets(mesh) 70 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_no_animation.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateNoAnimation(pyblish.api.Validator): 9 | """Ensure no keyframes on nodes in the Instance. 10 | 11 | Even though a Model would extract without animCurves correctly this avoids 12 | getting different output from a model when extracted from a different 13 | frame than the first frame. (Might be overly restrictive though) 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateContentsOrder 18 | label = "No Animation" 19 | hosts = ["maya"] 20 | families = ["colorbleed.model"] 21 | optional = True 22 | actions = [colorbleed.maya.action.SelectInvalidAction] 23 | 24 | def process(self, instance): 25 | 26 | invalid = self.get_invalid(instance) 27 | if invalid: 28 | raise RuntimeError("Keyframes found: {0}".format(invalid)) 29 | 30 | @staticmethod 31 | def get_invalid(instance): 32 | 33 | nodes = instance[:] 34 | if not nodes: 35 | return [] 36 | 37 | curves = cmds.keyframe(nodes, query=True, name=True) 38 | if curves: 39 | return list(set(cmds.listConnections(curves))) 40 | 41 | return [] 42 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_no_default_camera.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): 9 | """Ensure no default (startup) cameras are in the instance. 10 | 11 | This might be unnecessary. In the past there were some issues with 12 | referencing/importing files that contained the start up cameras overriding 13 | settings when being loaded and sometimes being skipped. 14 | """ 15 | 16 | order = colorbleed.api.ValidateContentsOrder 17 | hosts = ['maya'] 18 | families = ['colorbleed.camera'] 19 | version = (0, 1, 0) 20 | label = "No Default Cameras" 21 | actions = [colorbleed.maya.action.SelectInvalidAction] 22 | 23 | @staticmethod 24 | def get_invalid(instance): 25 | cameras = cmds.ls(instance, type='camera', long=True) 26 | return [cam for cam in cameras if 27 | cmds.camera(cam, query=True, startupCamera=True)] 28 | 29 | def process(self, instance): 30 | """Process all the cameras in the instance""" 31 | invalid = self.get_invalid(instance) 32 | assert not invalid, "Default cameras found: {0}".format(invalid) 33 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_no_namespace.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pm 2 | import maya.cmds as cmds 3 | 4 | import pyblish.api 5 | import colorbleed.api 6 | import colorbleed.maya.action 7 | 8 | 9 | def get_namespace(node_name): 10 | # ensure only node's name (not parent path) 11 | node_name = node_name.rsplit("|")[-1] 12 | # ensure only namespace 13 | return node_name.rpartition(":")[0] 14 | 15 | 16 | class ValidateNoNamespace(pyblish.api.InstancePlugin): 17 | """Ensure the nodes don't have a namespace""" 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | hosts = ['maya'] 21 | families = ['colorbleed.model'] 22 | category = 'cleanup' 23 | version = (0, 1, 0) 24 | label = 'No Namespaces' 25 | actions = [colorbleed.maya.action.SelectInvalidAction, 26 | colorbleed.api.RepairAction] 27 | 28 | @staticmethod 29 | def get_invalid(instance): 30 | nodes = cmds.ls(instance, long=True) 31 | return [node for node in nodes if get_namespace(node)] 32 | 33 | def process(self, instance): 34 | """Process all the nodes in the instance""" 35 | invalid = self.get_invalid(instance) 36 | 37 | if invalid: 38 | raise ValueError("Namespaces found: {0}".format(invalid)) 39 | 40 | @classmethod 41 | def repair(cls, instance): 42 | """Remove all namespaces from the nodes in the instance""" 43 | 44 | invalid = cls.get_invalid(instance) 45 | 46 | # Get nodes with pymel since we'll be renaming them 47 | # Since we don't want to keep checking the hierarchy 48 | # or full paths 49 | nodes = pm.ls(invalid) 50 | 51 | for node in nodes: 52 | namespace = node.namespace() 53 | if namespace: 54 | name = node.nodeName() 55 | node.rename(name[len(namespace):]) 56 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_no_unknown_nodes.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateNoUnknownNodes(pyblish.api.InstancePlugin): 9 | """Checks to see if there are any unknown nodes in the instance. 10 | 11 | This often happens if nodes from plug-ins are used but are not available 12 | on this machine. 13 | 14 | Note: Some studios use unknown nodes to store data on (as attributes) 15 | because it's a lightweight node. 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | hosts = ['maya'] 21 | families = ['colorbleed.model', 'colorbleed.rig'] 22 | optional = True 23 | label = "Unknown Nodes" 24 | actions = [colorbleed.maya.action.SelectInvalidAction] 25 | 26 | @staticmethod 27 | def get_invalid(instance): 28 | return cmds.ls(instance, type='unknown') 29 | 30 | def process(self, instance): 31 | """Process all the nodes in the instance""" 32 | 33 | invalid = self.get_invalid(instance) 34 | if invalid: 35 | raise ValueError("Unknown nodes found: {0}".format(invalid)) 36 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_no_vraymesh.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | from maya import cmds 3 | 4 | 5 | class ValidateNoVRayMesh(pyblish.api.InstancePlugin): 6 | """Validate there are no VRayMesh objects in the instance""" 7 | 8 | order = pyblish.api.ValidatorOrder 9 | label = 'No V-Ray Proxies (VRayMesh)' 10 | families = ["colorbleed.pointcache"] 11 | 12 | def process(self, instance): 13 | 14 | shapes = cmds.ls(instance, 15 | shapes=True, 16 | type="mesh") 17 | 18 | inputs = cmds.listConnections(shapes, 19 | destination=False, 20 | source=True) or [] 21 | vray_meshes = cmds.ls(inputs, type='VRayMesh') 22 | if vray_meshes: 23 | raise RuntimeError("Meshes that are VRayMeshes shouldn't " 24 | "be pointcached: {0}".format(vray_meshes)) 25 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_ids.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | from colorbleed.maya import lib 6 | 7 | 8 | class ValidateNodeIDs(pyblish.api.InstancePlugin): 9 | """Validate nodes have a Colorbleed Id. 10 | 11 | When IDs are missing from nodes *save your scene* and they should be 12 | automatically generated because IDs are created on non-referenced nodes 13 | in Maya upon scene save. 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidatePipelineOrder 18 | label = 'Instance Nodes Have ID' 19 | hosts = ['maya'] 20 | families = ["colorbleed.model", 21 | "colorbleed.look", 22 | "colorbleed.rig", 23 | "colorbleed.pointcache", 24 | "colorbleed.animation", 25 | "colorbleed.setdress", 26 | "colorbleed.yetiRig"] 27 | 28 | actions = [colorbleed.maya.action.SelectInvalidAction, 29 | colorbleed.maya.action.GenerateUUIDsOnInvalidAction] 30 | 31 | def process(self, instance): 32 | """Process all meshes""" 33 | 34 | # Ensure all nodes have a cbId 35 | invalid = self.get_invalid(instance) 36 | if invalid: 37 | raise RuntimeError("Nodes found without " 38 | "IDs: {0}".format(invalid)) 39 | 40 | @classmethod 41 | def get_invalid(cls, instance): 42 | """Return the member nodes that are invalid""" 43 | 44 | # We do want to check the referenced nodes as it might be 45 | # part of the end product. 46 | id_nodes = lib.get_id_required_nodes(referenced_nodes=True, 47 | nodes=instance[:]) 48 | invalid = [n for n in id_nodes if not lib.get_id(n)] 49 | 50 | return invalid 51 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_ids_deformed_shapes.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): 10 | """Validate if deformed shapes have related IDs to the original shapes. 11 | 12 | When a deformer is applied in the scene on a referenced mesh that already 13 | had deformers then Maya will create a new shape node for the mesh that 14 | does not have the original id. This validator checks whether the ids are 15 | valid on all the shape nodes in the instance. 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | families = ['colorbleed.look'] 21 | hosts = ['maya'] 22 | label = 'Deformed shape ids' 23 | actions = [colorbleed.maya.action.SelectInvalidAction, colorbleed.api.RepairAction] 24 | 25 | def process(self, instance): 26 | """Process all the nodes in the instance""" 27 | 28 | # Ensure all nodes have a cbId and a related ID to the original shapes 29 | # if a deformer has been created on the shape 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Shapes found that are considered 'Deformed'" 33 | "without object ids: {0}".format(invalid)) 34 | 35 | @classmethod 36 | def get_invalid(cls, instance): 37 | """Get all nodes which do not match the criteria""" 38 | 39 | shapes = cmds.ls(instance[:], 40 | dag=True, 41 | leaf=True, 42 | shapes=True, 43 | long=True, 44 | noIntermediate=True) 45 | 46 | invalid = [] 47 | for shape in shapes: 48 | history_id = lib.get_id_from_history(shape) 49 | if history_id: 50 | current_id = lib.get_id(shape) 51 | if current_id != history_id: 52 | invalid.append(shape) 53 | 54 | return invalid 55 | 56 | @classmethod 57 | def repair(cls, instance): 58 | 59 | for node in cls.get_invalid(instance): 60 | # Get the original id from history 61 | history_id = lib.get_id_from_history(node) 62 | if not history_id: 63 | cls.log.error("Could not find ID in history for '%s'", node) 64 | continue 65 | 66 | lib.set_id(node, history_id, overwrite=True) 67 | 68 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_ids_in_database.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import avalon.io as io 4 | 5 | import colorbleed.api 6 | import colorbleed.maya.action 7 | from colorbleed.maya import lib 8 | 9 | 10 | class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): 11 | """Validate if the CB Id is related to an asset in the database 12 | 13 | All nodes with the `cbId` attribute will be validated to ensure that 14 | the loaded asset in the scene is related to the current project. 15 | 16 | Tip: If there is an asset which is being reused from a different project 17 | please ensure the asset is republished in the new project 18 | 19 | """ 20 | 21 | order = colorbleed.api.ValidatePipelineOrder 22 | label = 'Node Ids in Database' 23 | hosts = ['maya'] 24 | families = ["*"] 25 | 26 | actions = [colorbleed.maya.action.SelectInvalidAction, 27 | colorbleed.maya.action.GenerateUUIDsOnInvalidAction] 28 | 29 | def process(self, instance): 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Found asset IDs which are not related to " 33 | "current project in instance: " 34 | "`%s`" % instance.name) 35 | 36 | @classmethod 37 | def get_invalid(cls, instance): 38 | 39 | invalid = [] 40 | 41 | # Get all id required nodes 42 | id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True, 43 | nodes=instance[:]) 44 | 45 | # check ids against database ids 46 | db_asset_ids = io.find({"type": "asset"}).distinct("_id") 47 | db_asset_ids = set(str(i) for i in db_asset_ids) 48 | 49 | # Get all asset IDs 50 | for node in id_required_nodes: 51 | cb_id = lib.get_id(node) 52 | 53 | # Ignore nodes without id, those are validated elsewhere 54 | if not cb_id: 55 | continue 56 | 57 | asset_id = cb_id.split(":", 1)[0] 58 | if asset_id not in db_asset_ids: 59 | cls.log.error("`%s` has unassociated asset ID" % node) 60 | invalid.append(node) 61 | 62 | return invalid 63 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_ids_related.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | import avalon.io as io 5 | import colorbleed.maya.action 6 | 7 | from colorbleed.maya import lib 8 | 9 | 10 | class ValidateNodeIDsRelated(pyblish.api.InstancePlugin): 11 | """Validate nodes have a related Colorbleed Id to the instance.data[asset] 12 | 13 | """ 14 | 15 | order = colorbleed.api.ValidatePipelineOrder 16 | label = 'Node Ids Related (ID)' 17 | hosts = ['maya'] 18 | families = ["colorbleed.model", 19 | "colorbleed.look", 20 | "colorbleed.rig"] 21 | optional = True 22 | 23 | actions = [colorbleed.maya.action.SelectInvalidAction, 24 | colorbleed.maya.action.GenerateUUIDsOnInvalidAction] 25 | 26 | def process(self, instance): 27 | """Process all nodes in instance (including hierarchy)""" 28 | # Ensure all nodes have a cbId 29 | invalid = self.get_invalid(instance) 30 | if invalid: 31 | raise RuntimeError("Nodes IDs found that are not related to asset " 32 | "'{}' : {}".format(instance.data['asset'], 33 | invalid)) 34 | 35 | @classmethod 36 | def get_invalid(cls, instance): 37 | """Return the member nodes that are invalid""" 38 | invalid = list() 39 | 40 | asset = instance.data['asset'] 41 | asset_data = io.find_one({"name": asset, 42 | "type": "asset"}, 43 | projection={"_id": True}) 44 | asset_id = str(asset_data['_id']) 45 | 46 | # We do want to check the referenced nodes as we it might be 47 | # part of the end product 48 | for node in instance: 49 | 50 | _id = lib.get_id(node) 51 | if not _id: 52 | continue 53 | 54 | node_asset_id = _id.split(":", 1)[0] 55 | if node_asset_id != asset_id: 56 | invalid.append(node) 57 | 58 | return invalid 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_ids_unique.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): 10 | """Validate the nodes in the instance have a unique Colorbleed Id 11 | 12 | Here we ensure that what has been added to the instance is unique 13 | """ 14 | 15 | order = colorbleed.api.ValidatePipelineOrder 16 | label = 'Non Duplicate Instance Members (ID)' 17 | hosts = ['maya'] 18 | families = ["colorbleed.model", 19 | "colorbleed.look", 20 | "colorbleed.rig", 21 | "colorbleed.yetiRig"] 22 | 23 | actions = [colorbleed.maya.action.SelectInvalidAction, 24 | colorbleed.maya.action.GenerateUUIDsOnInvalidAction] 25 | 26 | def process(self, instance): 27 | """Process all meshes""" 28 | 29 | # Ensure all nodes have a cbId 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Nodes found with non-unique " 33 | "asset IDs: {0}".format(invalid)) 34 | 35 | @classmethod 36 | def get_invalid(cls, instance): 37 | """Return the member nodes that are invalid""" 38 | 39 | # Check only non intermediate shapes 40 | # todo: must the instance itself ensure to have no intermediates? 41 | # todo: how come there are intermediates? 42 | from maya import cmds 43 | instance_members = cmds.ls(instance, noIntermediate=True, long=True) 44 | 45 | # Collect each id with their members 46 | ids = defaultdict(list) 47 | for member in instance_members: 48 | object_id = lib.get_id(member) 49 | if not object_id: 50 | continue 51 | ids[object_id].append(member) 52 | 53 | # Take only the ids with more than one member 54 | invalid = list() 55 | for _ids, members in ids.iteritems(): 56 | if len(members) > 1: 57 | cls.log.error("ID found on multiple nodes: '%s'" % members) 58 | invalid.extend(members) 59 | 60 | return invalid 61 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_node_no_ghosting.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): 9 | """Ensure nodes do not have ghosting enabled. 10 | 11 | If one would publish towards a non-Maya format it's likely that stats 12 | like ghosting won't be exported, eg. exporting to Alembic. 13 | 14 | Instead of creating many micro-managing checks (like this one) to ensure 15 | attributes have not been changed from their default it could be more 16 | efficient to export to a format that will never hold such data anyway. 17 | 18 | """ 19 | 20 | order = colorbleed.api.ValidateContentsOrder 21 | hosts = ['maya'] 22 | families = ['colorbleed.model', 'colorbleed.rig'] 23 | label = "No Ghosting" 24 | actions = [colorbleed.maya.action.SelectInvalidAction] 25 | 26 | _attributes = {'ghosting': 0} 27 | 28 | @classmethod 29 | def get_invalid(cls, instance): 30 | 31 | # Transforms and shapes seem to have ghosting 32 | nodes = cmds.ls(instance, long=True, type=['transform', 'shape']) 33 | invalid = [] 34 | for node in nodes: 35 | for attr, required_value in cls._attributes.iteritems(): 36 | if cmds.attributeQuery(attr, node=node, exists=True): 37 | 38 | value = cmds.getAttr('{0}.{1}'.format(node, attr)) 39 | if value != required_value: 40 | invalid.append(node) 41 | 42 | return invalid 43 | 44 | def process(self, instance): 45 | 46 | invalid = self.get_invalid(instance) 47 | 48 | if invalid: 49 | raise ValueError("Nodes with ghosting enabled found: " 50 | "{0}".format(invalid)) 51 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_render_image_rule.py: -------------------------------------------------------------------------------- 1 | import maya.mel as mel 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | 6 | 7 | def get_file_rule(rule): 8 | """Workaround for a bug in python with cmds.workspace""" 9 | return mel.eval('workspace -query -fileRuleEntry "{}"'.format(rule)) 10 | 11 | 12 | class ValidateRenderImageRule(pyblish.api.ContextPlugin): 13 | """Validates "images" file rule is set to "renders/" 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateContentsOrder 18 | label = "Images File Rule (Workspace)" 19 | hosts = ["maya"] 20 | families = ["colorbleed.renderlayer"] 21 | 22 | def process(self, context): 23 | 24 | assert get_file_rule("images") == "renders", ( 25 | "Workspace's `images` file rule must be set to: renders" 26 | ) 27 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_render_no_default_cameras.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): 9 | """Ensure no default (startup) cameras are to be rendered.""" 10 | 11 | order = colorbleed.api.ValidateContentsOrder 12 | hosts = ['maya'] 13 | families = ['colorbleed.renderlayer'] 14 | label = "No Default Cameras Renderable" 15 | actions = [colorbleed.maya.action.SelectInvalidAction] 16 | 17 | @staticmethod 18 | def get_invalid(instance): 19 | 20 | renderable = set(instance.data["cameras"]) 21 | 22 | # Collect default cameras 23 | cameras = cmds.ls(type='camera', long=True) 24 | defaults = set(cam for cam in cameras if 25 | cmds.camera(cam, query=True, startupCamera=True)) 26 | 27 | return [cam for cam in renderable if cam in defaults] 28 | 29 | def process(self, instance): 30 | """Process all the cameras in the instance""" 31 | invalid = self.get_invalid(instance) 32 | if invalid: 33 | raise RuntimeError("Renderable default cameras " 34 | "found: {0}".format(invalid)) 35 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_render_single_camera.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | 6 | class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): 7 | """Only one camera may be renderable in a layer. 8 | 9 | Currently the pipeline supports only a single camera per layer. 10 | This is because when multiple cameras are rendered the output files 11 | automatically get different names because the render token 12 | is not in the output path. As such the output files conflict with how 13 | our pipeline expects the output. 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateContentsOrder 18 | label = "Render Single Camera" 19 | hosts = ['maya'] 20 | families = ["colorbleed.renderlayer", 21 | "colorbleed.vrayscene"] 22 | actions = [colorbleed.maya.action.SelectInvalidAction] 23 | 24 | def process(self, instance): 25 | """Process all the cameras in the instance""" 26 | invalid = self.get_invalid(instance) 27 | if invalid: 28 | raise RuntimeError("Invalid cameras for render.") 29 | 30 | @classmethod 31 | def get_invalid(cls, instance): 32 | 33 | cameras = instance.data.get("cameras", []) 34 | 35 | if len(cameras) > 1: 36 | cls.log.error("Multiple renderable cameras found for %s: %s " % 37 | (instance.data["setMembers"], cameras)) 38 | return [instance.data["setMembers"]] + cameras 39 | 40 | elif len(cameras) < 1: 41 | cls.log.error("No renderable cameras found for %s " % 42 | instance.data["setMembers"]) 43 | return [instance.data["setMembers"]] 44 | 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_renderlayer_aovs.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import colorbleed.maya.action 4 | from avalon import io 5 | import colorbleed.api 6 | 7 | 8 | class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): 9 | """Validate created AOVs / RenderElement is registered in the database 10 | 11 | Each render element is registered as a subset which is formatted based on 12 | the render layer and the render element, example: 13 | 14 | . 15 | 16 | This translates to something like this: 17 | 18 | CHAR.diffuse 19 | 20 | This check is needed to ensure the render output is still complete 21 | 22 | """ 23 | 24 | order = pyblish.api.ValidatorOrder + 0.1 25 | label = "Render Passes / AOVs Are Registered" 26 | hosts = ["maya"] 27 | families = ["colorbleed.renderlayer"] 28 | actions = [colorbleed.maya.action.SelectInvalidAction] 29 | 30 | def process(self, instance): 31 | invalid = self.get_invalid(instance) 32 | if invalid: 33 | raise RuntimeError("Found unregistered subsets: {}".format(invalid)) 34 | 35 | def get_invalid(self, instance): 36 | 37 | invalid = [] 38 | 39 | asset_name = instance.data["asset"] 40 | render_passses = instance.data.get("renderPasses", []) 41 | for render_pass in render_passses: 42 | is_valid = self.validate_subset_registered(asset_name, render_pass) 43 | if not is_valid: 44 | invalid.append(render_pass) 45 | 46 | return invalid 47 | 48 | def validate_subset_registered(self, asset_name, subset_name): 49 | """Check if subset is registered in the database under the asset""" 50 | 51 | asset = io.find_one({"type": "asset", "name": asset_name}) 52 | is_valid = io.find_one({"type": "subset", 53 | "name": subset_name, 54 | "parent": asset["_id"]}) 55 | 56 | return is_valid 57 | 58 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_rig_out_set_node_ids.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | import colorbleed.maya.lib as lib 7 | 8 | 9 | class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): 10 | """Validate if deformed shapes have related IDs to the original shapes. 11 | 12 | When a deformer is applied in the scene on a referenced mesh that already 13 | had deformers then Maya will create a new shape node for the mesh that 14 | does not have the original id. This validator checks whether the ids are 15 | valid on all the shape nodes in the instance. 16 | 17 | """ 18 | 19 | order = colorbleed.api.ValidateContentsOrder 20 | families = ["colorbleed.rig"] 21 | hosts = ['maya'] 22 | label = 'Rig Out Set Node Ids' 23 | actions = [colorbleed.maya.action.SelectInvalidAction, colorbleed.api.RepairAction] 24 | 25 | def process(self, instance): 26 | """Process all meshes""" 27 | 28 | # Ensure all nodes have a cbId and a related ID to the original shapes 29 | # if a deformer has been created on the shape 30 | invalid = self.get_invalid(instance) 31 | if invalid: 32 | raise RuntimeError("Nodes found with non-related " 33 | "asset IDs: {0}".format(invalid)) 34 | 35 | @classmethod 36 | def get_invalid(cls, instance): 37 | """Get all nodes which do not match the criteria""" 38 | 39 | invalid = [] 40 | 41 | out_set = next(x for x in instance if x.endswith("out_SET")) 42 | members = cmds.sets(out_set, query=True) 43 | shapes = cmds.ls(members, 44 | dag=True, 45 | leaf=True, 46 | shapes=True, 47 | long=True, 48 | noIntermediate=True) 49 | 50 | for shape in shapes: 51 | history_id = lib.get_id_from_history(shape) 52 | if history_id: 53 | current_id = lib.get_id(shape) 54 | if current_id != history_id: 55 | invalid.append(shape) 56 | 57 | return invalid 58 | 59 | @classmethod 60 | def repair(cls, instance): 61 | 62 | for node in cls.get_invalid(instance): 63 | # Get the original id from history 64 | history_id = lib.get_id_from_history(node) 65 | if not history_id: 66 | cls.log.error("Could not find ID in history for '%s'", node) 67 | continue 68 | 69 | lib.set_id(node, history_id, overwrite=True) 70 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_scene_set_workspace.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import maya.cmds as cmds 4 | 5 | import pyblish.api 6 | import colorbleed.api 7 | 8 | 9 | def is_subdir(path, root_dir): 10 | """ Returns whether path is a subdirectory (or file) within root_dir """ 11 | path = os.path.realpath(path) 12 | root_dir = os.path.realpath(root_dir) 13 | 14 | # If not on same drive 15 | if os.path.splitdrive(path)[0] != os.path.splitdrive(root_dir)[0]: 16 | return False 17 | 18 | # Get 'relative path' (can contain ../ which means going up) 19 | relative = os.path.relpath(path, root_dir) 20 | 21 | # Check if the path starts by going up, if so it's not a subdirectory. :) 22 | if relative.startswith(os.pardir) or relative == os.curdir: 23 | return False 24 | else: 25 | return True 26 | 27 | 28 | class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): 29 | """Validate the scene is inside the currently set Maya workspace""" 30 | 31 | order = colorbleed.api.ValidatePipelineOrder 32 | hosts = ['maya'] 33 | category = 'scene' 34 | version = (0, 1, 0) 35 | label = 'Maya Workspace Set' 36 | 37 | def process(self, context): 38 | 39 | scene_name = cmds.file(query=True, sceneName=True) 40 | if not scene_name: 41 | raise RuntimeError("Scene hasn't been saved. Workspace can't be " 42 | "validated.") 43 | 44 | root_dir = cmds.workspace(query=True, rootDirectory=True) 45 | 46 | if not is_subdir(scene_name, root_dir): 47 | raise RuntimeError("Maya workspace is not set correctly.") 48 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_setdress_namespaces.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | 6 | class ValidateSetdressNamespaces(pyblish.api.InstancePlugin): 7 | """Ensure namespaces are not nested 8 | 9 | In the outliner an item in a normal namespace looks as following: 10 | props_desk_01_:modelDefault 11 | 12 | Any namespace which diverts from that is illegal, example of an illegal 13 | namespace: 14 | room_study_01_:props_desk_01_:modelDefault 15 | 16 | """ 17 | 18 | label = "Validate Setdress Namespaces" 19 | order = pyblish.api.ValidatorOrder 20 | families = ["colorbleed.setdress"] 21 | actions = [colorbleed.maya.action.SelectInvalidAction] 22 | 23 | def process(self, instance): 24 | 25 | self.log.info("Checking namespace for %s" % instance.name) 26 | if self.get_invalid(instance): 27 | raise RuntimeError("Nested namespaces found") 28 | 29 | @classmethod 30 | def get_invalid(cls, instance): 31 | 32 | from maya import cmds 33 | 34 | invalid = [] 35 | for item in cmds.ls(instance): 36 | item_parts = item.split("|", 1)[0].rsplit(":") 37 | if len(item_parts[:-1]) > 1: 38 | invalid.append(item) 39 | 40 | return invalid 41 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_shape_render_stats.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | from maya import cmds 5 | 6 | import colorbleed.maya.action 7 | 8 | 9 | class ValidateShapeRenderStats(pyblish.api.Validator): 10 | """Ensure all render stats are set to the default values.""" 11 | 12 | order = colorbleed.api.ValidateMeshOrder 13 | hosts = ['maya'] 14 | families = ['colorbleed.model'] 15 | label = 'Shape Default Render Stats' 16 | actions = [colorbleed.maya.action.SelectInvalidAction, 17 | colorbleed.api.RepairAction] 18 | 19 | defaults = {'castsShadows': 1, 20 | 'receiveShadows': 1, 21 | 'motionBlur': 1, 22 | 'primaryVisibility': 1, 23 | 'smoothShading': 1, 24 | 'visibleInReflections': 1, 25 | 'visibleInRefractions': 1, 26 | 'doubleSided': 1, 27 | 'opposite': 0} 28 | 29 | @classmethod 30 | def get_invalid(cls, instance): 31 | # It seems the "surfaceShape" and those derived from it have 32 | # `renderStat` attributes. 33 | shapes = cmds.ls(instance, long=True, type='surfaceShape') 34 | invalid = [] 35 | for shape in shapes: 36 | for attr, default_value in cls.defaults.iteritems(): 37 | if cmds.attributeQuery(attr, node=shape, exists=True): 38 | value = cmds.getAttr('{}.{}'.format(shape, attr)) 39 | if value != default_value: 40 | invalid.append(shape) 41 | 42 | return invalid 43 | 44 | def process(self, instance): 45 | 46 | invalid = self.get_invalid(instance) 47 | 48 | if invalid: 49 | raise ValueError("Shapes with non-default renderStats " 50 | "found: {0}".format(invalid)) 51 | 52 | @classmethod 53 | def repair(cls, instance): 54 | for shape in cls.get_invalid(instance): 55 | for attr, default_value in cls.defaults.iteritems(): 56 | 57 | if cmds.attributeQuery(attr, node=shape, exists=True): 58 | plug = '{0}.{1}'.format(shape, attr) 59 | value = cmds.getAttr(plug) 60 | if value != default_value: 61 | cmds.setAttr(plug, default_value) 62 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_single_assembly.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | 5 | class ValidateSingleAssembly(pyblish.api.InstancePlugin): 6 | """Ensure the content of the instance is grouped in a single hierarchy 7 | 8 | The instance must have a single root node containing all the content. 9 | This root node *must* be a top group in the outliner. 10 | 11 | Example outliner: 12 | root_GRP 13 | -- geometry_GRP 14 | -- mesh_GEO 15 | -- controls_GRP 16 | -- control_CTL 17 | 18 | """ 19 | 20 | order = colorbleed.api.ValidateContentsOrder 21 | hosts = ['maya'] 22 | families = ['colorbleed.rig', 'colorbleed.animation'] 23 | label = 'Single Assembly' 24 | 25 | def process(self, instance): 26 | from maya import cmds 27 | 28 | assemblies = cmds.ls(instance, assemblies=True) 29 | 30 | # ensure unique (somehow `maya.cmds.ls` doesn't manage that) 31 | assemblies = set(assemblies) 32 | 33 | assert len(assemblies) > 0, ( 34 | "One assembly required for: %s (currently empty?)" % instance) 35 | assert len(assemblies) < 2, ( 36 | 'Multiple assemblies found: %s' % assemblies) 37 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_step_size.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.action 4 | 5 | 6 | class ValidateStepSize(pyblish.api.InstancePlugin): 7 | """Validates the step size for the instance is in a valid range. 8 | 9 | For example the `step` size should never be lower or equal to zero. 10 | 11 | """ 12 | 13 | order = colorbleed.api.ValidateContentsOrder 14 | label = 'Step size' 15 | families = ['colorbleed.camera', 16 | 'colorbleed.pointcache', 17 | 'colorbleed.animation'] 18 | actions = [colorbleed.maya.action.SelectInvalidAction] 19 | 20 | MIN = 0.01 21 | MAX = 1.0 22 | 23 | @classmethod 24 | def get_invalid(cls, instance): 25 | 26 | objset = instance.data['name'] 27 | step = instance.data.get("step", 1.0) 28 | 29 | if step < cls.MIN or step > cls.MAX: 30 | cls.log.warning("Step size is outside of valid range: {0} " 31 | "(valid: {1} to {2})".format(step, 32 | cls.MIN, 33 | cls.MAX)) 34 | return objset 35 | 36 | return [] 37 | 38 | def process(self, instance): 39 | 40 | invalid = self.get_invalid(instance) 41 | if invalid: 42 | raise RuntimeError("Invalid instances found: {0}".format(invalid)) 43 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_transfers.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import os 4 | 5 | from collections import defaultdict 6 | 7 | 8 | class ValidateTransfers(pyblish.api.InstancePlugin): 9 | """Validates mapped resources. 10 | 11 | This validates: 12 | - The resources all transfer to a unique destination. 13 | 14 | """ 15 | 16 | order = colorbleed.api.ValidateContentsOrder 17 | label = "Transfers" 18 | 19 | def process(self, instance): 20 | 21 | transfers = instance.data.get("transfers", []) 22 | if not transfers: 23 | return 24 | 25 | # Collect all destination with its sources 26 | collected = defaultdict(set) 27 | for source, destination in transfers: 28 | 29 | # Use normalized paths in comparison and ignore case sensitivity 30 | source = os.path.normpath(source).lower() 31 | destination = os.path.normpath(destination).lower() 32 | 33 | collected[destination].add(source) 34 | 35 | invalid_destinations = list() 36 | for destination, sources in collected.items(): 37 | if len(sources) > 1: 38 | invalid_destinations.append(destination) 39 | 40 | self.log.error("Non-unique file transfer for resources: " 41 | "{0} (sources: {1})".format(destination, 42 | list(sources))) 43 | 44 | if invalid_destinations: 45 | raise RuntimeError("Invalid transfers in queue.") 46 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_transform_zero.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateTransformZero(pyblish.api.Validator): 9 | """Transforms can't have any values 10 | 11 | To solve this issue, try freezing the transforms. So long 12 | as the transforms, rotation and scale values are zero, 13 | you're all good. 14 | 15 | """ 16 | 17 | order = colorbleed.api.ValidateContentsOrder 18 | hosts = ["maya"] 19 | families = ["colorbleed.model"] 20 | category = "geometry" 21 | version = (0, 1, 0) 22 | label = "Transform Zero (Freeze)" 23 | actions = [colorbleed.maya.action.SelectInvalidAction] 24 | 25 | _identity = [1.0, 0.0, 0.0, 0.0, 26 | 0.0, 1.0, 0.0, 0.0, 27 | 0.0, 0.0, 1.0, 0.0, 28 | 0.0, 0.0, 0.0, 1.0] 29 | _tolerance = 1e-30 30 | 31 | @classmethod 32 | def get_invalid(cls, instance): 33 | """Returns the invalid transforms in the instance. 34 | 35 | This is the same as checking: 36 | - translate == [0, 0, 0] and rotate == [0, 0, 0] and 37 | scale == [1, 1, 1] and shear == [0, 0, 0] 38 | 39 | .. note:: 40 | This will also catch camera transforms if those 41 | are in the instances. 42 | 43 | Returns: 44 | list: Transforms that are not identity matrix 45 | 46 | """ 47 | 48 | transforms = cmds.ls(instance, type="transform") 49 | 50 | invalid = [] 51 | for transform in transforms: 52 | mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True) 53 | if not all(abs(x-y) < cls._tolerance 54 | for x, y in zip(cls._identity, mat)): 55 | invalid.append(transform) 56 | 57 | return invalid 58 | 59 | def process(self, instance): 60 | """Process all the nodes in the instance "objectSet""" 61 | 62 | invalid = self.get_invalid(instance) 63 | if invalid: 64 | raise ValueError("Nodes found with transform " 65 | "values: {0}".format(invalid)) 66 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_vray_distributed_rendering.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | import colorbleed.maya.lib as lib 4 | 5 | from maya import cmds 6 | 7 | 8 | class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): 9 | """Validate V-Ray Distributed Rendering is ignored in batch mode. 10 | 11 | Whenever Distributed Rendering is enabled for V-Ray in the render settings 12 | ensure that the "Ignore in batch mode" is enabled so the submitted job 13 | won't try to render each frame with all machines resulting in faulty 14 | errors. 15 | 16 | """ 17 | 18 | order = colorbleed.api.ValidateContentsOrder 19 | label = "VRay Distributed Rendering" 20 | families = ["colorbleed.renderlayer"] 21 | actions = [colorbleed.api.RepairAction] 22 | 23 | # V-Ray attribute names 24 | enabled_attr = "vraySettings.sys_distributed_rendering_on" 25 | ignored_attr = "vraySettings.sys_distributed_rendering_ignore_batch" 26 | 27 | def process(self, instance): 28 | 29 | if instance.data.get("renderer") != "vray": 30 | # If not V-Ray ignore.. 31 | return 32 | 33 | vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode") 34 | assert vray_settings, "Please ensure a VRay Settings Node is present" 35 | 36 | renderlayer = instance.data['setMembers'] 37 | 38 | if not lib.get_attr_in_layer(self.enabled_attr, layer=renderlayer): 39 | # If not distributed rendering enabled, ignore.. 40 | return 41 | 42 | # If distributed rendering is enabled but it is *not* set to ignore 43 | # during batch mode we invalidate the instance 44 | if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer): 45 | raise RuntimeError("Renderlayer has distributed rendering enabled " 46 | "but is not set to ignore in batch mode.") 47 | 48 | @classmethod 49 | def repair(cls, instance): 50 | 51 | renderlayer = instance.data.get("setMembers") 52 | with lib.renderlayer(renderlayer): 53 | cls.log.info("Enabling Distributed Rendering " 54 | "ignore in batch mode..") 55 | cmds.setAttr(cls.ignored_attr, True) 56 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_vray_translator_settings.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | from colorbleed.plugin import contextplugin_should_run 4 | 5 | from maya import cmds 6 | 7 | 8 | class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): 9 | 10 | order = colorbleed.api.ValidateContentsOrder 11 | label = "VRay Translator Settings" 12 | families = ["colorbleed.vrayscene"] 13 | actions = [colorbleed.api.RepairContextAction] 14 | 15 | def process(self, context): 16 | 17 | # Workaround bug pyblish-base#250 18 | if not contextplugin_should_run(self, context): 19 | return 20 | 21 | invalid = self.get_invalid(context) 22 | if invalid: 23 | raise RuntimeError("Found invalid VRay Translator settings!") 24 | 25 | @classmethod 26 | def get_invalid(cls, context): 27 | 28 | invalid = False 29 | 30 | # Get vraySettings node 31 | vray_settings = cmds.ls(type="VRaySettingsNode") 32 | assert vray_settings, "Please ensure a VRay Settings Node is present" 33 | 34 | node = vray_settings[0] 35 | 36 | if cmds.setAttr("{}.vrscene_render_on".format(node)): 37 | cls.log.error("Render is enabled, this should be disabled") 38 | invalid = True 39 | 40 | if not cmds.getAttr("{}.vrscene_on".format(node)): 41 | cls.log.error("Export vrscene not enabled") 42 | invalid = True 43 | 44 | if not cmds.getAttr("{}.misc_eachFrameInFile".format(node)): 45 | cls.log.error("Each Frame in File not enabled") 46 | invalid = True 47 | 48 | vrscene_filename = cmds.getAttr("{}.vrscene_filename".format(node)) 49 | if vrscene_filename != "vrayscene//_/": 50 | cls.log.error("Template for file name is wrong") 51 | invalid = True 52 | 53 | return invalid 54 | 55 | @classmethod 56 | def repair(cls, context): 57 | 58 | vray_settings = cmds.ls(type="VRaySettingsNode") 59 | if not vray_settings: 60 | node = cmds.createNode("VRaySettingsNode") 61 | else: 62 | node = vray_settings[0] 63 | 64 | cmds.setAttr("{}.vrscene_render_on".format(node), False) 65 | cmds.setAttr("{}.vrscene_on".format(node), True) 66 | cmds.setAttr("{}.misc_eachFrameInFile".format(node), True) 67 | cmds.setAttr("{}.vrscene_filename".format(node), 68 | "vrayscene//_/", 69 | type="string") 70 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_vrayproxy_members.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import colorbleed.api 3 | 4 | from maya import cmds 5 | 6 | import colorbleed.maya.action 7 | 8 | 9 | class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): 10 | """Validate whether the V-Ray Proxy instance has shape members""" 11 | 12 | order = pyblish.api.ValidatorOrder 13 | label = 'VRay Proxy Members' 14 | hosts = ['maya'] 15 | families = ['colorbleed.vrayproxy'] 16 | actions = [colorbleed.maya.action.SelectInvalidAction] 17 | 18 | def process(self, instance): 19 | 20 | invalid = self.get_invalid(instance) 21 | 22 | if invalid: 23 | raise RuntimeError("'%s' is invalid VRay Proxy for " 24 | "export!" % instance.name) 25 | 26 | @classmethod 27 | def get_invalid(cls, instance): 28 | 29 | shapes = cmds.ls(instance, 30 | shapes=True, 31 | noIntermediate=True, 32 | long=True) 33 | 34 | if not shapes: 35 | cls.log.error("'%s' contains no shapes." % instance.name) 36 | 37 | # Return the instance itself 38 | return [instance.name] 39 | 40 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_yeti_rig_input_in_instance.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import pyblish.api 4 | import colorbleed.api 5 | import colorbleed.maya.action 6 | 7 | 8 | class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): 9 | """Validate if all input nodes are part of the instance's hierarchy""" 10 | 11 | order = colorbleed.api.ValidateContentsOrder 12 | hosts = ["maya"] 13 | families = ["colorbleed.yetiRig"] 14 | label = "Yeti Rig Input Shapes In Instance" 15 | actions = [colorbleed.maya.action.SelectInvalidAction] 16 | 17 | def process(self, instance): 18 | 19 | invalid = self.get_invalid(instance) 20 | if invalid: 21 | raise RuntimeError("Yeti Rig has invalid input meshes") 22 | 23 | @classmethod 24 | def get_invalid(cls, instance): 25 | 26 | input_set = next((i for i in instance if i == "input_SET"), None) 27 | assert input_set, "Current %s instance has no `input_SET`" % instance 28 | 29 | # Get all children, we do not care about intermediates 30 | input_nodes = cmds.ls(cmds.sets(input_set, query=True), long=True) 31 | dag = cmds.ls(input_nodes, dag=True, long=True) 32 | shapes = cmds.ls(dag, long=True, shapes=True, noIntermediate=True) 33 | 34 | # Allow publish without input meshes. 35 | if not shapes: 36 | cls.log.info("Found no input meshes for %s, skipping ..." 37 | % instance) 38 | return [] 39 | 40 | # check if input node is part of groomRig instance 41 | instance_lookup = set(instance[:]) 42 | invalid = [s for s in shapes if s not in instance_lookup] 43 | 44 | return invalid 45 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_yeti_rig_settings.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | class ValidateYetiRigSettings(pyblish.api.InstancePlugin): 5 | """Validate Yeti Rig Settings have collected input connections. 6 | 7 | The input connections are collected for the nodes in the `input_SET`. 8 | When no input connections are found a warning is logged but it is allowed 9 | to pass validation. 10 | 11 | """ 12 | 13 | order = pyblish.api.ValidatorOrder 14 | label = "Yeti Rig Settings" 15 | families = ["colorbleed.yetiRig"] 16 | 17 | def process(self, instance): 18 | 19 | invalid = self.get_invalid(instance) 20 | if invalid: 21 | raise RuntimeError("Detected invalid Yeti Rig data. (See log) " 22 | "Tip: Save the scene") 23 | 24 | @classmethod 25 | def get_invalid(cls, instance): 26 | 27 | rigsettings = instance.data.get("rigsettings", None) 28 | if rigsettings is None: 29 | cls.log.error("MAJOR ERROR: No rig settings found!") 30 | return True 31 | 32 | # Get inputs 33 | inputs = rigsettings.get("inputs", []) 34 | if not inputs: 35 | # Empty rig settings dictionary 36 | cls.log.warning("No rig inputs found. This can happen when " 37 | "the rig has no inputs from outside the rig.") 38 | return False 39 | 40 | for input in inputs: 41 | source_id = input["sourceID"] 42 | if source_id is None: 43 | cls.log.error("Discovered source with 'None' as ID, please " 44 | "check if the input shape has a cbId") 45 | return True 46 | 47 | destination_id = input["destinationID"] 48 | if destination_id is None: 49 | cls.log.error("Discovered None as destination ID value") 50 | return True 51 | 52 | return False 53 | -------------------------------------------------------------------------------- /colorbleed/plugins/maya/publish/validate_yetirig_cache_state.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | import colorbleed.action 4 | 5 | import maya.cmds as cmds 6 | 7 | import colorbleed.maya.action 8 | 9 | 10 | class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): 11 | """Validate the I/O attributes of the node 12 | 13 | Every pgYetiMaya cache node per instance should have: 14 | 1. Input Mode is set to `None` 15 | 2. Input Cache File Name is empty 16 | 17 | """ 18 | 19 | order = pyblish.api.ValidatorOrder 20 | label = "Yeti Rig Cache State" 21 | hosts = ["maya"] 22 | families = ["colorbleed.yetiRig"] 23 | actions = [colorbleed.action.RepairAction, 24 | colorbleed.maya.action.SelectInvalidAction] 25 | 26 | def process(self, instance): 27 | invalid = self.get_invalid(instance) 28 | if invalid: 29 | raise RuntimeError("Nodes have incorrect I/O settings") 30 | 31 | @classmethod 32 | def get_invalid(cls, instance): 33 | 34 | invalid = [] 35 | 36 | yeti_nodes = cmds.ls(instance, type="pgYetiMaya") 37 | for node in yeti_nodes: 38 | # Check reading state 39 | state = cmds.getAttr("%s.fileMode" % node) 40 | if state == 1: 41 | cls.log.error("Node `%s` is set to mode `cache`" % node) 42 | invalid.append(node) 43 | continue 44 | 45 | # Check reading state 46 | has_cache = cmds.getAttr("%s.cacheFileName" % node) 47 | if has_cache: 48 | cls.log.error("Node `%s` has a cache file set" % node) 49 | invalid.append(node) 50 | continue 51 | 52 | return invalid 53 | 54 | @classmethod 55 | def repair(cls, instance): 56 | """Repair all errors""" 57 | 58 | # Create set to ensure all nodes only pass once 59 | invalid = cls.get_invalid(instance) 60 | for node in invalid: 61 | cmds.setAttr("%s.fileMode" % node, 0) 62 | cmds.setAttr("%s.cacheFileName" % node, "", type="string") 63 | 64 | -------------------------------------------------------------------------------- /colorbleed/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/colorbleed/scripts/__init__.py -------------------------------------------------------------------------------- /colorbleed/vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/colorbleed/vendor/__init__.py -------------------------------------------------------------------------------- /colorbleed/vendor/pather/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Roy Nieterau' 2 | 3 | 4 | from .core import * 5 | from .version import * 6 | -------------------------------------------------------------------------------- /colorbleed/vendor/pather/error.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ParseError(ValueError): 4 | """Error raised when parsing a path with a pattern fails""" 5 | pass 6 | -------------------------------------------------------------------------------- /colorbleed/vendor/pather/version.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION_MAJOR = 0 3 | VERSION_MINOR = 1 4 | VERSION_PATCH = 1 5 | 6 | version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 7 | version = '%i.%i.%i' % version_info 8 | __version__ = version 9 | 10 | __all__ = ['version', 'version_info', '__version__'] 11 | -------------------------------------------------------------------------------- /colorbleed/version.py: -------------------------------------------------------------------------------- 1 | version = "1.0.4" 2 | -------------------------------------------------------------------------------- /colorbleed/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/colorbleed/widgets/__init__.py -------------------------------------------------------------------------------- /res/icons/colorbleed_logo_36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/res/icons/colorbleed_logo_36x36.png -------------------------------------------------------------------------------- /res/icons/inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/res/icons/inventory.png -------------------------------------------------------------------------------- /res/icons/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/res/icons/loader.png -------------------------------------------------------------------------------- /res/icons/workfiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colorbleed/colorbleed-config/5c9dab597f2382e7297b002a30f2bc3886677366/res/icons/workfiles.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # Support for both Python 2 and 3 3 | universal=1 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """The colorbleed production pipeline""" 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | 7 | classifiers = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "License :: OSI Approved :: MIT License", 10 | "Intended Audience :: Developers", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 2", 14 | "Programming Language :: Python :: 2.7", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.6", 17 | "Programming Language :: Python :: 3.7", 18 | "Topic :: Utilities", 19 | "Topic :: Software Development", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | ] 22 | 23 | exclude = [ 24 | "__pycache__", 25 | ] 26 | 27 | here = os.path.dirname(__file__) 28 | package_dir = os.path.join(here, "colorbleed") 29 | version = None 30 | 31 | # Read version from file, to avoid importing anything 32 | mod = {} 33 | with open(os.path.join(package_dir, "version.py")) as f: 34 | exec(compile(f.read(), f.name, 'exec'), mod) 35 | version = mod["version"] 36 | 37 | assert len(version.split(".")) == 3, version 38 | 39 | package_data = [] 40 | for base, dirs, files in os.walk(package_dir): 41 | dirs[:] = [d for d in dirs if d not in exclude] 42 | relpath = os.path.relpath(base, package_dir) 43 | basename = os.path.basename(base) 44 | 45 | for fname in files: 46 | if any(fname.endswith(pat) for pat in exclude): 47 | continue 48 | 49 | fname = os.path.join(relpath, fname) 50 | package_data += [fname] 51 | 52 | setup( 53 | name="avalon-colorbleed", 54 | version=version, 55 | url="https://github.com/Colorbleed/colorbleed-config", 56 | author="Roy Nieterau", 57 | author_email="roy@colorbleed.com", 58 | license="MIT", 59 | zip_safe=False, 60 | packages=find_packages(), 61 | package_data={ 62 | "colorbleed": package_data, 63 | }, 64 | classifiers=classifiers, 65 | install_requires=[ 66 | "pyblish-base>=1.5", 67 | "avalon-core>=5.2", 68 | ], 69 | python_requires=">2.7, <4", 70 | ) 71 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/32bit/backgrounds_selected_to32bit.py: -------------------------------------------------------------------------------- 1 | from avalon.fusion import comp_lock_and_undo_chunk 2 | 3 | 4 | def main(): 5 | """Set all selected backgrounds to 32 bit""" 6 | with comp_lock_and_undo_chunk(comp, 'Selected Backgrounds to 32bit'): 7 | tools = comp.GetToolList(True, "Background").values() 8 | for tool in tools: 9 | tool.Depth = 5 10 | 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/32bit/backgrounds_to32bit.py: -------------------------------------------------------------------------------- 1 | from avalon.fusion import comp_lock_and_undo_chunk 2 | 3 | 4 | def main(): 5 | """Set all backgrounds to 32 bit""" 6 | with comp_lock_and_undo_chunk(comp, 'Backgrounds to 32bit'): 7 | tools = comp.GetToolList(False, "Background").values() 8 | for tool in tools: 9 | tool.Depth = 5 10 | 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/32bit/loaders_selected_to32bit.py: -------------------------------------------------------------------------------- 1 | from avalon.fusion import comp_lock_and_undo_chunk 2 | 3 | 4 | def main(): 5 | """Set all selected loaders to 32 bit""" 6 | with comp_lock_and_undo_chunk(comp, 'Selected Loaders to 32bit'): 7 | tools = comp.GetToolList(True, "Loader").values() 8 | for tool in tools: 9 | tool.Depth = 5 10 | 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/32bit/loaders_to32bit.py: -------------------------------------------------------------------------------- 1 | from avalon.fusion import comp_lock_and_undo_chunk 2 | 3 | 4 | def main(): 5 | """Set all loaders to 32 bit""" 6 | with comp_lock_and_undo_chunk(comp, 'Loaders to 32bit'): 7 | tools = comp.GetToolList(False, "Loader").values() 8 | for tool in tools: 9 | tool.Depth = 5 10 | 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/duplicate_with_input_connections.py: -------------------------------------------------------------------------------- 1 | from avalon.fusion import comp_lock_and_undo_chunk 2 | 3 | 4 | def is_connected(input): 5 | """Return whether an input has incoming connection""" 6 | return input.GetAttrs()["INPB_Connected"] 7 | 8 | 9 | def duplicate_with_input_connections(): 10 | """Duplicate selected tools with incoming connections.""" 11 | 12 | original_tools = comp.GetToolList(True).values() 13 | if not original_tools: 14 | return # nothing selected 15 | 16 | with comp_lock_and_undo_chunk(comp, "Duplicate With Input Connections"): 17 | 18 | # Generate duplicates 19 | comp.Copy() 20 | comp.SetActiveTool() 21 | comp.Paste() 22 | duplicate_tools = comp.GetToolList(True).values() 23 | 24 | # Copy connections 25 | for original, new in zip(original_tools, duplicate_tools): 26 | 27 | original_inputs = original.GetInputList().values() 28 | new_inputs = new.GetInputList().values() 29 | assert len(original_inputs) == len(new_inputs) 30 | 31 | for original_input, new_input in zip(original_inputs, new_inputs): 32 | 33 | if is_connected(original_input): 34 | 35 | if is_connected(new_input): 36 | # Already connected if it is between the copied tools 37 | continue 38 | 39 | new_input.ConnectTo(original_input.GetConnectedOutput()) 40 | assert is_connected(new_input), "Must be connected now" 41 | 42 | 43 | duplicate_with_input_connections() 44 | -------------------------------------------------------------------------------- /setup/fusion/scripts/Comp/colorbleed/update_selected_loader_ranges.py: -------------------------------------------------------------------------------- 1 | """Forces Fusion to 'retrigger' the Loader to update. 2 | 3 | Warning: 4 | This might change settings like 'Reverse', 'Loop', trims and other 5 | settings of the Loader. So use this at your own risk. 6 | 7 | """ 8 | 9 | from avalon.fusion import comp_lock_and_undo_chunk 10 | 11 | 12 | with comp_lock_and_undo_chunk(comp, "Reload clip time ranges"): 13 | tools = comp.GetToolList(True, "Loader").values() 14 | for tool in tools: 15 | 16 | # Get tool attributes 17 | tool_a = tool.GetAttrs() 18 | clipTable = tool_a['TOOLST_Clip_Name'] 19 | altclipTable = tool_a['TOOLST_AltClip_Name'] 20 | startTime = tool_a['TOOLNT_Clip_Start'] 21 | old_global_in = tool.GlobalIn[comp.CurrentTime] 22 | 23 | # Reapply 24 | for index, _ in clipTable.items(): 25 | time = startTime[index] 26 | tool.Clip[time] = tool.Clip[time] 27 | 28 | for index, _ in altclipTable.items(): 29 | time = startTime[index] 30 | tool.ProxyFilename[time] = tool.ProxyFilename[time] 31 | 32 | tool.GlobalIn[comp.CurrentTime] = old_global_in 33 | --------------------------------------------------------------------------------