├── .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 |
--------------------------------------------------------------------------------