Merge remote-tracking branch 'upstream/develop' into enhancement/tweak_logging

# Conflicts:
#	openpype/plugins/publish/extract_hierarchy_to_ayon.py
This commit is contained in:
Roy Nieterau 2023-09-05 12:06:43 +02:00
commit d36b2f1514
99 changed files with 1385 additions and 1295 deletions

View file

@ -35,6 +35,7 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.16.5-nightly.4
- 3.16.5-nightly.3
- 3.16.5-nightly.2
- 3.16.5-nightly.1
@ -134,7 +135,6 @@ body:
- 3.14.9-nightly.1
- 3.14.8
- 3.14.8-nightly.4
- 3.14.8-nightly.3
validations:
required: true
- type: dropdown

View file

@ -62,7 +62,7 @@ development tools like [CMake](https://cmake.org/) and [Visual Studio](https://v
#### Clone repository:
```sh
git clone --recurse-submodules git@github.com:Pypeclub/OpenPype.git
git clone --recurse-submodules git@github.com:ynput/OpenPype.git
```
#### To build OpenPype:

View file

@ -45,6 +45,9 @@ class OCIOEnvHook(PreLaunchHook):
if config_data:
ocio_path = config_data["path"]
if self.host_name in ["nuke", "hiero"]:
ocio_path = ocio_path.replace("\\", "/")
self.log.info(
f"Setting OCIO environment to config path: {ocio_path}")

View file

@ -164,7 +164,7 @@ class RenderCreator(Creator):
api.get_stub().rename_item(comp_id,
new_comp_name)
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["aftereffects"]["create"]["RenderCreator"]
)

View file

@ -138,7 +138,6 @@ class CollectAERender(publish.AbstractCollectRender):
fam = "render.farm"
if fam not in instance.families:
instance.families.append(fam)
instance.toBeRenderedOn = "deadline"
instance.renderer = "aerender"
instance.farm = True # to skip integrate
if "review" in instance.families:

View file

@ -1,16 +0,0 @@
from openpype.hosts.fusion.api import (
comp_lock_and_undo_chunk,
get_current_comp
)
def main():
comp = get_current_comp()
"""Set all selected backgrounds to 32 bit"""
with comp_lock_and_undo_chunk(comp, 'Selected Backgrounds to 32bit'):
tools = comp.GetToolList(True, "Background").values()
for tool in tools:
tool.Depth = 5
main()

View file

@ -1,16 +0,0 @@
from openpype.hosts.fusion.api import (
comp_lock_and_undo_chunk,
get_current_comp
)
def main():
comp = get_current_comp()
"""Set all backgrounds to 32 bit"""
with comp_lock_and_undo_chunk(comp, 'Backgrounds to 32bit'):
tools = comp.GetToolList(False, "Background").values()
for tool in tools:
tool.Depth = 5
main()

View file

@ -1,16 +0,0 @@
from openpype.hosts.fusion.api import (
comp_lock_and_undo_chunk,
get_current_comp
)
def main():
comp = get_current_comp()
"""Set all selected loaders to 32 bit"""
with comp_lock_and_undo_chunk(comp, 'Selected Loaders to 32bit'):
tools = comp.GetToolList(True, "Loader").values()
for tool in tools:
tool.Depth = 5
main()

View file

@ -1,16 +0,0 @@
from openpype.hosts.fusion.api import (
comp_lock_and_undo_chunk,
get_current_comp
)
def main():
comp = get_current_comp()
"""Set all loaders to 32 bit"""
with comp_lock_and_undo_chunk(comp, 'Loaders to 32bit'):
tools = comp.GetToolList(False, "Loader").values()
for tool in tools:
tool.Depth = 5
main()

View file

@ -1,200 +0,0 @@
import os
import sys
import glob
import logging
from qtpy import QtWidgets, QtCore
import qtawesome as qta
from openpype.client import get_assets
from openpype import style
from openpype.pipeline import (
install_host,
get_current_project_name,
)
from openpype.hosts.fusion import api
from openpype.pipeline.context_tools import get_workdir_from_session
log = logging.getLogger("Fusion Switch Shot")
class App(QtWidgets.QWidget):
def __init__(self, parent=None):
################################################
# |---------------------| |------------------| #
# |Comp | |Asset | #
# |[..][ v]| |[ v]| #
# |---------------------| |------------------| #
# | Update existing comp [ ] | #
# |------------------------------------------| #
# | Switch | #
# |------------------------------------------| #
################################################
QtWidgets.QWidget.__init__(self, parent)
layout = QtWidgets.QVBoxLayout()
# Comp related input
comp_hlayout = QtWidgets.QHBoxLayout()
comp_label = QtWidgets.QLabel("Comp file")
comp_label.setFixedWidth(50)
comp_box = QtWidgets.QComboBox()
button_icon = qta.icon("fa.folder", color="white")
open_from_dir = QtWidgets.QPushButton()
open_from_dir.setIcon(button_icon)
comp_box.setFixedHeight(25)
open_from_dir.setFixedWidth(25)
open_from_dir.setFixedHeight(25)
comp_hlayout.addWidget(comp_label)
comp_hlayout.addWidget(comp_box)
comp_hlayout.addWidget(open_from_dir)
# Asset related input
asset_hlayout = QtWidgets.QHBoxLayout()
asset_label = QtWidgets.QLabel("Shot")
asset_label.setFixedWidth(50)
asset_box = QtWidgets.QComboBox()
asset_box.setLineEdit(QtWidgets.QLineEdit())
asset_box.setFixedHeight(25)
refresh_icon = qta.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton()
refresh_btn.setIcon(refresh_icon)
asset_box.setFixedHeight(25)
refresh_btn.setFixedWidth(25)
refresh_btn.setFixedHeight(25)
asset_hlayout.addWidget(asset_label)
asset_hlayout.addWidget(asset_box)
asset_hlayout.addWidget(refresh_btn)
# Options
options = QtWidgets.QHBoxLayout()
options.setAlignment(QtCore.Qt.AlignLeft)
current_comp_check = QtWidgets.QCheckBox()
current_comp_check.setChecked(True)
current_comp_label = QtWidgets.QLabel("Use current comp")
options.addWidget(current_comp_label)
options.addWidget(current_comp_check)
accept_btn = QtWidgets.QPushButton("Switch")
layout.addLayout(options)
layout.addLayout(comp_hlayout)
layout.addLayout(asset_hlayout)
layout.addWidget(accept_btn)
self._open_from_dir = open_from_dir
self._comps = comp_box
self._assets = asset_box
self._use_current = current_comp_check
self._accept_btn = accept_btn
self._refresh_btn = refresh_btn
self.setWindowTitle("Fusion Switch Shot")
self.setLayout(layout)
self.resize(260, 140)
self.setMinimumWidth(260)
self.setFixedHeight(140)
self.connections()
# Update ui to correct state
self._on_use_current_comp()
self._refresh()
def connections(self):
self._use_current.clicked.connect(self._on_use_current_comp)
self._open_from_dir.clicked.connect(self._on_open_from_dir)
self._refresh_btn.clicked.connect(self._refresh)
self._accept_btn.clicked.connect(self._on_switch)
def _on_use_current_comp(self):
state = self._use_current.isChecked()
self._open_from_dir.setEnabled(not state)
self._comps.setEnabled(not state)
def _on_open_from_dir(self):
start_dir = get_workdir_from_session()
comp_file, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "Choose comp", start_dir)
if not comp_file:
return
# Create completer
self.populate_comp_box([comp_file])
self._refresh()
def _refresh(self):
# Clear any existing items
self._assets.clear()
asset_names = self.collect_asset_names()
completer = QtWidgets.QCompleter(asset_names)
self._assets.setCompleter(completer)
self._assets.addItems(asset_names)
def _on_switch(self):
if not self._use_current.isChecked():
file_name = self._comps.itemData(self._comps.currentIndex())
else:
comp = api.get_current_comp()
file_name = comp.GetAttrs("COMPS_FileName")
asset = self._assets.currentText()
import colorbleed.scripts.fusion_switch_shot as switch_shot
switch_shot.switch(asset_name=asset, filepath=file_name, new=True)
def collect_slap_comps(self, directory):
items = glob.glob("{}/*.comp".format(directory))
return items
def collect_asset_names(self):
project_name = get_current_project_name()
asset_docs = get_assets(project_name, fields=["name"])
asset_names = {
asset_doc["name"]
for asset_doc in asset_docs
}
return list(asset_names)
def populate_comp_box(self, files):
"""Ensure we display the filename only but the path is stored as well
Args:
files (list): list of full file path [path/to/item/item.ext,]
Returns:
None
"""
for f in files:
filename = os.path.basename(f)
self._comps.addItem(filename, userData=f)
if __name__ == '__main__':
install_host(api)
app = QtWidgets.QApplication(sys.argv)
window = App()
window.setStyleSheet(style.load_stylesheet())
window.show()
sys.exit(app.exec_())

View file

@ -1,40 +0,0 @@
"""Forces Fusion to 'retrigger' the Loader to update.
Warning:
This might change settings like 'Reverse', 'Loop', trims and other
settings of the Loader. So use this at your own risk.
"""
from openpype.hosts.fusion.api.pipeline import (
get_current_comp,
comp_lock_and_undo_chunk
)
def update_loader_ranges():
comp = get_current_comp()
with comp_lock_and_undo_chunk(comp, "Reload clip time ranges"):
tools = comp.GetToolList(True, "Loader").values()
for tool in tools:
# Get tool attributes
tool_a = tool.GetAttrs()
clipTable = tool_a['TOOLST_Clip_Name']
altclipTable = tool_a['TOOLST_AltClip_Name']
startTime = tool_a['TOOLNT_Clip_Start']
old_global_in = tool.GlobalIn[comp.CurrentTime]
# Reapply
for index, _ in clipTable.items():
time = startTime[index]
tool.Clip[time] = tool.Clip[time]
for index, _ in altclipTable.items():
time = startTime[index]
tool.ProxyFilename[time] = tool.ProxyFilename[time]
tool.GlobalIn[comp.CurrentTime] = old_global_in
if __name__ == '__main__':
update_loader_ranges()

View file

@ -5,7 +5,7 @@ Global = {
Map = {
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
["Config:"] = "UserPaths:Config;OpenPype:Config",
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts",
},
},
Script = {

View file

@ -30,10 +30,6 @@ class CreateSaver(NewCreator):
instance_attributes = [
"reviewable"
]
default_variants = [
"Main",
"Mask"
]
# TODO: This should be renamed together with Nuke so it is aligned
temp_rendering_path_template = (
@ -250,11 +246,7 @@ class CreateSaver(NewCreator):
label="Review",
)
def apply_settings(
self,
project_settings,
system_settings
):
def apply_settings(self, project_settings):
"""Method called on initialization of plugin to apply settings."""
# plugin settings

View file

@ -108,7 +108,6 @@ class CollectFusionRender(
fam = "render.farm"
if fam not in instance.families:
instance.families.append(fam)
instance.toBeRenderedOn = "deadline"
instance.farm = True # to skip integrate
if "review" in instance.families:
# to skip ExtractReview locally

View file

@ -147,13 +147,13 @@ class CollectFarmRender(publish.AbstractCollectRender):
attachTo=False,
setMembers=[node],
publish=info[4],
review=False,
renderer=None,
priority=50,
name=node.split("/")[1],
family="render.farm",
families=["render.farm"],
farm=True,
resolutionWidth=context.data["resolutionWidth"],
resolutionHeight=context.data["resolutionHeight"],
@ -174,7 +174,6 @@ class CollectFarmRender(publish.AbstractCollectRender):
outputFormat=info[1],
outputStartFrame=info[3],
leadingZeros=info[2],
toBeRenderedOn='deadline',
ignoreFrameHandleCheck=True
)

View file

@ -57,28 +57,31 @@ def create_interactive(creator_identifier, **kwargs):
list: The created instances.
"""
# TODO Use Qt instead
result, variant = hou.ui.readInput('Define variant name',
buttons=("Ok", "Cancel"),
initial_contents='Main',
title="Define variant",
help="Set the variant for the "
"publish instance",
close_choice=1)
if result == 1:
# User interrupted
return
variant = variant.strip()
if not variant:
raise RuntimeError("Empty variant value entered.")
host = registered_host()
context = CreateContext(host)
creator = context.manual_creators.get(creator_identifier)
if not creator:
raise RuntimeError("Invalid creator identifier: "
"{}".format(creator_identifier))
raise RuntimeError("Invalid creator identifier: {}".format(
creator_identifier)
)
# TODO Use Qt instead
result, variant = hou.ui.readInput(
"Define variant name",
buttons=("Ok", "Cancel"),
initial_contents=creator.get_default_variant(),
title="Define variant",
help="Set the variant for the publish instance",
close_choice=1
)
if result == 1:
# User interrupted
return
variant = variant.strip()
if not variant:
raise RuntimeError("Empty variant value entered.")
# TODO: Once more elaborate unique create behavior should exist per Creator
# instead of per network editor area then we should move this from here

View file

@ -22,9 +22,12 @@ log = logging.getLogger(__name__)
JSON_PREFIX = "JSON:::"
def get_asset_fps():
def get_asset_fps(asset_doc=None):
"""Return current asset fps."""
return get_current_project_asset()["data"].get("fps")
if asset_doc is None:
asset_doc = get_current_project_asset(fields=["data.fps"])
return asset_doc["data"]["fps"]
def set_id(node, unique_id, overwrite=False):
@ -472,14 +475,19 @@ def maintained_selection():
def reset_framerange():
"""Set frame range to current asset"""
"""Set frame range and FPS to current asset"""
# Get asset data
project_name = get_current_project_name()
asset_name = get_current_asset_name()
# Get the asset ID from the database for the asset of current context
asset_doc = get_asset_by_name(project_name, asset_name)
asset_data = asset_doc["data"]
# Get FPS
fps = get_asset_fps(asset_doc)
# Get Start and End Frames
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
@ -493,6 +501,9 @@ def reset_framerange():
frame_start -= int(handle_start)
frame_end += int(handle_end)
# Set frame range and FPS
print("Setting scene FPS to {}".format(int(fps)))
set_scene_fps(fps)
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)

View file

@ -25,7 +25,6 @@ from openpype.lib import (
emit_event,
)
from .lib import get_asset_fps
log = logging.getLogger("openpype.hosts.houdini")
@ -385,11 +384,6 @@ def _set_context_settings():
None
"""
# Set new scene fps
fps = get_asset_fps()
print("Setting scene FPS to %i" % fps)
lib.set_scene_fps(fps)
lib.reset_framerange()

View file

@ -296,7 +296,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
"""
return [hou.ropNodeTypeCategory()]
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
"""Method called on initialization of plugin to apply settings."""
settings_name = self.settings_name

View file

@ -33,7 +33,7 @@ class CreateVDBCache(plugin.HoudiniCreator):
}
if self.selected_nodes:
parms["soppath"] = self.selected_nodes[0].path()
parms["soppath"] = self.get_sop_node_path(self.selected_nodes[0])
instance_node.setParms(parms)
@ -42,3 +42,63 @@ class CreateVDBCache(plugin.HoudiniCreator):
hou.ropNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
def get_sop_node_path(self, selected_node):
"""Get Sop Path of the selected node.
Although Houdini allows ObjNode path on `sop_path` for the
the ROP node, we prefer it set to the SopNode path explicitly.
"""
# Allow sop level paths (e.g. /obj/geo1/box1)
if isinstance(selected_node, hou.SopNode):
self.log.debug(
"Valid SopNode selection, 'SOP Path' in ROP will"
" be set to '%s'.", selected_node.path()
)
return selected_node.path()
# Allow object level paths to Geometry nodes (e.g. /obj/geo1)
# but do not allow other object level nodes types like cameras, etc.
elif isinstance(selected_node, hou.ObjNode) and \
selected_node.type().name() == "geo":
# Try to find output node.
sop_node = self.get_obj_output(selected_node)
if sop_node:
self.log.debug(
"Valid ObjNode selection, 'SOP Path' in ROP will "
"be set to the child path '%s'.", sop_node.path()
)
return sop_node.path()
self.log.debug(
"Selection isn't valid. 'SOP Path' in ROP will be empty."
)
return ""
def get_obj_output(self, obj_node):
"""Try to find output node.
If any output nodes are present, return the output node with
the minimum 'outputidx'
If no output nodes are present, return the node with display flag
If no nodes are present at all, return None
"""
outputs = obj_node.subnetOutputs()
# if obj_node is empty
if not outputs:
return
# if obj_node has one output child whether its
# sop output node or a node with the render flag
elif len(outputs) == 1:
return outputs[0]
# if there are more than one, then it has multiple output nodes
# return the one with the minimum 'outputidx'
else:
return min(outputs,
key=lambda node: node.evalParm('outputidx'))

View file

@ -59,6 +59,9 @@ class HdaLoader(load.LoaderPlugin):
def_paths = [d.libraryFilePath() for d in defs]
new = def_paths.index(file_path)
defs[new].setIsPreferred(True)
hda_node.setParms({
"representation": str(representation["_id"])
})
def remove(self, container):
node = container["node"]

View file

@ -6,7 +6,7 @@ from typing import Any, Dict, Union
import six
from openpype.pipeline.context_tools import (
get_current_project, get_current_project_asset,)
get_current_project, get_current_project_asset)
from pymxs import runtime as rt
JSON_PREFIX = "JSON::"
@ -312,3 +312,98 @@ def set_timeline(frameStart, frameEnd):
"""
rt.animationRange = rt.interval(frameStart, frameEnd)
return rt.animationRange
def unique_namespace(namespace, format="%02d",
prefix="", suffix="", con_suffix="CON"):
"""Return unique namespace
Arguments:
namespace (str): Name of namespace to consider
format (str, optional): Formatting of the given iteration number
suffix (str, optional): Only consider namespaces with this suffix.
con_suffix: max only, for finding the name of the master container
>>> unique_namespace("bar")
# bar01
>>> unique_namespace(":hello")
# :hello01
>>> unique_namespace("bar:", suffix="_NS")
# bar01_NS:
"""
def current_namespace():
current = namespace
# When inside a namespace Max adds no trailing :
if not current.endswith(":"):
current += ":"
return current
# Always check against the absolute namespace root
# There's no clash with :x if we're defining namespace :a:x
ROOT = ":" if namespace.startswith(":") else current_namespace()
# Strip trailing `:` tokens since we might want to add a suffix
start = ":" if namespace.startswith(":") else ""
end = ":" if namespace.endswith(":") else ""
namespace = namespace.strip(":")
if ":" in namespace:
# Split off any nesting that we don't uniqify anyway.
parents, namespace = namespace.rsplit(":", 1)
start += parents + ":"
ROOT += start
iteration = 1
increment_version = True
while increment_version:
nr_namespace = namespace + format % iteration
unique = prefix + nr_namespace + suffix
container_name = f"{unique}:{namespace}{con_suffix}"
if not rt.getNodeByName(container_name):
name_space = start + unique + end
increment_version = False
return name_space
else:
increment_version = True
iteration += 1
def get_namespace(container_name):
"""Get the namespace and name of the sub-container
Args:
container_name (str): the name of master container
Raises:
RuntimeError: when there is no master container found
Returns:
namespace (str): namespace of the sub-container
name (str): name of the sub-container
"""
node = rt.getNodeByName(container_name)
if not node:
raise RuntimeError("Master Container Not Found..")
name = rt.getUserProp(node, "name")
namespace = rt.getUserProp(node, "namespace")
return namespace, name
def object_transform_set(container_children):
"""A function which allows to store the transform of
previous loaded object(s)
Args:
container_children(list): A list of nodes
Returns:
transform_set (dict): A dict with all transform data of
the previous loaded object(s)
"""
transform_set = {}
for node in container_children:
name = f"{node.name}.transform"
transform_set[name] = node.pos
name = f"{node.name}.scale"
transform_set[name] = node.scale
return transform_set

View file

@ -43,7 +43,7 @@ class RenderSettings(object):
rt.viewport.setCamera(sel)
break
if not found:
raise RuntimeError("Camera not found")
raise RuntimeError("Active Camera not found")
def render_output(self, container):
folder = rt.maxFilePath
@ -113,7 +113,8 @@ class RenderSettings(object):
# for setting up renderable camera
arv = rt.MAXToAOps.ArnoldRenderView()
render_camera = rt.viewport.GetCamera()
arv.setOption("Camera", str(render_camera))
if render_camera:
arv.setOption("Camera", str(render_camera))
# TODO: add AOVs and extension
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa

View file

@ -15,8 +15,10 @@ from openpype.pipeline import (
)
from openpype.hosts.max.api.menu import OpenPypeMenu
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB
from openpype.hosts.max import MAX_HOST_DIR
from pymxs import runtime as rt # noqa
log = logging.getLogger("openpype.hosts.max")
@ -152,17 +154,18 @@ def ls() -> list:
yield lib.read(container)
def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"):
def containerise(name: str, nodes: list, context,
namespace=None, loader=None, suffix="_CON"):
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": "",
"namespace": namespace or "",
"loader": loader,
"representation": context["representation"]["_id"],
}
container_name = f"{name}{suffix}"
container_name = f"{namespace}:{name}{suffix}"
container = rt.container(name=container_name)
for node in nodes:
node.Parent = container
@ -170,3 +173,52 @@ def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"):
if not lib.imprint(container_name, data):
print(f"imprinting of {container_name} failed.")
return container
def load_custom_attribute_data():
"""Re-loading the Openpype/AYON custom parameter built by the creator
Returns:
attribute: re-loading the custom OP attributes set in Maxscript
"""
return rt.Execute(MS_CUSTOM_ATTRIB)
def import_custom_attribute_data(container: str, selections: list):
"""Importing the Openpype/AYON custom parameter built by the creator
Args:
container (str): target container which adds custom attributes
selections (list): nodes to be added into
group in custom attributes
"""
attrs = load_custom_attribute_data()
modifier = rt.EmptyModifier()
rt.addModifier(container, modifier)
container.modifiers[0].name = "OP Data"
rt.custAttributes.add(container.modifiers[0], attrs)
nodes = {}
for i in selections:
nodes = {
str(i): rt.NodeTransformMonitor(node=i),
}
# Setting the property
rt.setProperty(
container.modifiers[0].openPypeData,
"all_handles", nodes.values())
rt.setProperty(
container.modifiers[0].openPypeData,
"sel_list", nodes.keys())
def update_custom_attribute_data(container: str, selections: list):
"""Updating the Openpype/AYON custom parameter built by the creator
Args:
container (str): target container which adds custom attributes
selections (list): nodes to be added into
group in custom attributes
"""
if container.modifiers[0].name == "OP Data":
rt.deleteModifier(container, container.modifiers[0])
import_custom_attribute_data(container, selections)

View file

@ -1,7 +1,16 @@
import os
from openpype.hosts.max.api import lib, maintained_selection
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.lib import (
unique_namespace,
get_namespace,
object_transform_set
)
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.pipeline import get_representation_path, load
@ -13,50 +22,76 @@ class FbxLoader(load.LoaderPlugin):
order = -9
icon = "code-fork"
color = "white"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
filepath = self.filepath_from_context(context)
filepath = os.path.normpath(filepath)
rt.FBXImporterSetParam("Animation", True)
rt.FBXImporterSetParam("Camera", True)
rt.FBXImporterSetParam("AxisConversionMethod", True)
rt.FBXImporterSetParam("Mode", rt.Name("create"))
rt.FBXImporterSetParam("Preserveinstances", True)
rt.ImportFile(
filepath,
rt.name("noPrompt"),
using=rt.FBXIMP)
container = rt.GetNodeByName(f"{name}")
if not container:
container = rt.Container()
container.name = f"{name}"
namespace = unique_namespace(
name + "_",
suffix="_",
)
container = rt.container(
name=f"{namespace}:{name}_{self.postfix}")
selections = rt.GetCurrentSelection()
import_custom_attribute_data(container, selections)
for selection in rt.GetCurrentSelection():
for selection in selections:
selection.Parent = container
selection.name = f"{namespace}:{selection.name}"
return containerise(
name, [container], context, loader=self.__class__.__name__)
name, [container], context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.GetNodeByName(container["instance_node"])
rt.Select(node.Children)
fbx_reimport_cmd = (
f"""
node_name = container["instance_node"]
node = rt.getNodeByName(node_name)
namespace, name = get_namespace(node_name)
sub_node_name = f"{namespace}:{name}_{self.postfix}"
inst_container = rt.getNodeByName(sub_node_name)
rt.Select(inst_container.Children)
transform_data = object_transform_set(inst_container.Children)
for prev_fbx_obj in rt.selection:
if rt.isValidNode(prev_fbx_obj):
rt.Delete(prev_fbx_obj)
FBXImporterSetParam "Animation" true
FBXImporterSetParam "Cameras" true
FBXImporterSetParam "AxisConversionMethod" true
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
rt.FBXImporterSetParam("Animation", True)
rt.FBXImporterSetParam("Camera", True)
rt.FBXImporterSetParam("Mode", rt.Name("merge"))
rt.FBXImporterSetParam("AxisConversionMethod", True)
rt.FBXImporterSetParam("Preserveinstances", True)
rt.ImportFile(
path, rt.name("noPrompt"), using=rt.FBXIMP)
current_fbx_objects = rt.GetCurrentSelection()
for fbx_object in current_fbx_objects:
if fbx_object.Parent != inst_container:
fbx_object.Parent = inst_container
fbx_object.name = f"{namespace}:{fbx_object.name}"
fbx_object.pos = transform_data[
f"{fbx_object.name}.transform"]
fbx_object.scale = transform_data[
f"{fbx_object.name}.scale"]
importFile @"{path}" #noPrompt using:FBXIMP
""")
rt.Execute(fbx_reimport_cmd)
for children in node.Children:
if rt.classOf(children) == rt.Container:
if children.name == sub_node_name:
update_custom_attribute_data(
children, current_fbx_objects)
with maintained_selection():
rt.Select(node)

View file

@ -1,7 +1,15 @@
import os
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.lib import (
unique_namespace,
get_namespace,
object_transform_set
)
from openpype.hosts.max.api.pipeline import (
containerise, import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.pipeline import get_representation_path, load
@ -16,22 +24,34 @@ class MaxSceneLoader(load.LoaderPlugin):
order = -8
icon = "code-fork"
color = "green"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
path = self.filepath_from_context(context)
path = os.path.normpath(path)
# import the max scene by using "merge file"
path = path.replace('\\', '/')
rt.MergeMaxFile(path)
rt.MergeMaxFile(path, quiet=True, includeFullGroup=True)
max_objects = rt.getLastMergedNodes()
max_container = rt.Container(name=f"{name}")
for max_object in max_objects:
max_object.Parent = max_container
max_object_names = [obj.name for obj in max_objects]
# implement the OP/AYON custom attributes before load
max_container = []
namespace = unique_namespace(
name + "_",
suffix="_",
)
container_name = f"{namespace}:{name}_{self.postfix}"
container = rt.Container(name=container_name)
import_custom_attribute_data(container, max_objects)
max_container.append(container)
max_container.extend(max_objects)
for max_obj, obj_name in zip(max_objects, max_object_names):
max_obj.name = f"{namespace}:{obj_name}"
return containerise(
name, [max_container], context, loader=self.__class__.__name__)
name, max_container, context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
@ -39,15 +59,32 @@ class MaxSceneLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node_name = container["instance_node"]
rt.MergeMaxFile(path,
rt.Name("noRedraw"),
rt.Name("deleteOldDups"),
rt.Name("useSceneMtlDups"))
node = rt.getNodeByName(node_name)
namespace, name = get_namespace(node_name)
sub_container_name = f"{namespace}:{name}_{self.postfix}"
# delete the old container with attribute
# delete old duplicate
rt.Select(node.Children)
transform_data = object_transform_set(node.Children)
for prev_max_obj in rt.GetCurrentSelection():
if rt.isValidNode(prev_max_obj) and prev_max_obj.name != sub_container_name: # noqa
rt.Delete(prev_max_obj)
rt.MergeMaxFile(path, rt.Name("deleteOldDups"))
max_objects = rt.getLastMergedNodes()
container_node = rt.GetNodeByName(node_name)
for max_object in max_objects:
max_object.Parent = container_node
current_max_objects = rt.getLastMergedNodes()
current_max_object_names = [obj.name for obj
in current_max_objects]
sub_container = rt.getNodeByName(sub_container_name)
update_custom_attribute_data(sub_container, current_max_objects)
for max_object in current_max_objects:
max_object.Parent = node
for max_obj, obj_name in zip(current_max_objects,
current_max_object_names):
max_obj.name = f"{namespace}:{obj_name}"
max_obj.pos = transform_data[
f"{max_obj.name}.transform"]
max_obj.scale = transform_data[
f"{max_obj.name}.scale"]
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])

View file

@ -1,8 +1,14 @@
import os
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
from openpype.hosts.max.api.lib import (
maintained_selection, unique_namespace
)
class ModelAbcLoader(load.LoaderPlugin):
@ -14,6 +20,7 @@ class ModelAbcLoader(load.LoaderPlugin):
order = -10
icon = "code-fork"
color = "orange"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -30,7 +37,7 @@ class ModelAbcLoader(load.LoaderPlugin):
rt.AlembicImport.CustomAttributes = True
rt.AlembicImport.UVs = True
rt.AlembicImport.VertexColors = True
rt.importFile(file_path, rt.name("noPrompt"))
rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport)
abc_after = {
c
@ -45,9 +52,22 @@ class ModelAbcLoader(load.LoaderPlugin):
self.log.error("Something failed when loading.")
abc_container = abc_containers.pop()
import_custom_attribute_data(
abc_container, abc_container.Children)
namespace = unique_namespace(
name + "_",
suffix="_",
)
for abc_object in abc_container.Children:
abc_object.name = f"{namespace}:{abc_object.name}"
# rename the abc container with namespace
abc_container_name = f"{namespace}:{name}_{self.postfix}"
abc_container.name = abc_container_name
return containerise(
name, [abc_container], context, loader=self.__class__.__name__
name, [abc_container], context,
namespace, loader=self.__class__.__name__
)
def update(self, container, representation):
@ -55,21 +75,19 @@ class ModelAbcLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node = rt.GetNodeByName(container["instance_node"])
rt.Select(node.Children)
for alembic in rt.Selection:
abc = rt.GetNodeByName(alembic.name)
rt.Select(abc.Children)
for abc_con in rt.Selection:
container = rt.GetNodeByName(abc_con.name)
container.source = path
rt.Select(container.Children)
for abc_obj in rt.Selection:
alembic_obj = rt.GetNodeByName(abc_obj.name)
alembic_obj.source = path
with maintained_selection():
rt.Select(node)
rt.Select(node.Children)
for alembic in rt.Selection:
abc = rt.GetNodeByName(alembic.name)
update_custom_attribute_data(abc, abc.Children)
rt.Select(abc.Children)
for abc_con in abc.Children:
abc_con.source = path
rt.Select(abc_con.Children)
for abc_obj in abc_con.Children:
abc_obj.source = path
lib.imprint(
container["instance_node"],

View file

@ -1,7 +1,15 @@
import os
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.pipeline import (
containerise, import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import (
unique_namespace,
get_namespace,
object_transform_set
)
from openpype.hosts.max.api.lib import maintained_selection
@ -13,6 +21,7 @@ class FbxModelLoader(load.LoaderPlugin):
order = -9
icon = "code-fork"
color = "white"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -20,39 +29,69 @@ class FbxModelLoader(load.LoaderPlugin):
filepath = os.path.normpath(self.filepath_from_context(context))
rt.FBXImporterSetParam("Animation", False)
rt.FBXImporterSetParam("Cameras", False)
rt.FBXImporterSetParam("Mode", rt.Name("create"))
rt.FBXImporterSetParam("Preserveinstances", True)
rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP)
container = rt.GetNodeByName(name)
if not container:
container = rt.Container()
container.name = name
namespace = unique_namespace(
name + "_",
suffix="_",
)
container = rt.container(
name=f"{namespace}:{name}_{self.postfix}")
selections = rt.GetCurrentSelection()
import_custom_attribute_data(container, selections)
for selection in rt.GetCurrentSelection():
for selection in selections:
selection.Parent = container
selection.name = f"{namespace}:{selection.name}"
return containerise(
name, [container], context, loader=self.__class__.__name__
name, [container], context,
namespace, loader=self.__class__.__name__
)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
rt.select(node.Children)
node_name = container["instance_node"]
node = rt.getNodeByName(node_name)
namespace, name = get_namespace(node_name)
sub_node_name = f"{namespace}:{name}_{self.postfix}"
inst_container = rt.getNodeByName(sub_node_name)
rt.Select(inst_container.Children)
transform_data = object_transform_set(inst_container.Children)
for prev_fbx_obj in rt.selection:
if rt.isValidNode(prev_fbx_obj):
rt.Delete(prev_fbx_obj)
rt.FBXImporterSetParam("Animation", False)
rt.FBXImporterSetParam("Cameras", False)
rt.FBXImporterSetParam("Mode", rt.Name("merge"))
rt.FBXImporterSetParam("AxisConversionMethod", True)
rt.FBXImporterSetParam("UpAxis", "Y")
rt.FBXImporterSetParam("Preserveinstances", True)
rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP)
current_fbx_objects = rt.GetCurrentSelection()
for fbx_object in current_fbx_objects:
if fbx_object.Parent != inst_container:
fbx_object.Parent = inst_container
fbx_object.name = f"{namespace}:{fbx_object.name}"
fbx_object.pos = transform_data[
f"{fbx_object.name}.transform"]
fbx_object.scale = transform_data[
f"{fbx_object.name}.scale"]
for children in node.Children:
if rt.classOf(children) == rt.Container:
if children.name == sub_node_name:
update_custom_attribute_data(
children, current_fbx_objects)
with maintained_selection():
rt.Select(node)
lib.imprint(
container["instance_node"],
node_name,
{"representation": str(representation["_id"])},
)

View file

@ -1,8 +1,18 @@
import os
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import (
unique_namespace,
get_namespace,
maintained_selection,
object_transform_set
)
from openpype.hosts.max.api.lib import maintained_selection
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.pipeline import get_representation_path, load
@ -14,6 +24,7 @@ class ObjLoader(load.LoaderPlugin):
order = -9
icon = "code-fork"
color = "white"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -22,36 +33,49 @@ class ObjLoader(load.LoaderPlugin):
self.log.debug("Executing command to import..")
rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp')
namespace = unique_namespace(
name + "_",
suffix="_",
)
# create "missing" container for obj import
container = rt.Container()
container.name = name
container = rt.Container(name=f"{namespace}:{name}_{self.postfix}")
selections = rt.GetCurrentSelection()
import_custom_attribute_data(container, selections)
# get current selection
for selection in rt.GetCurrentSelection():
for selection in selections:
selection.Parent = container
asset = rt.GetNodeByName(name)
selection.name = f"{namespace}:{selection.name}"
return containerise(
name, [asset], context, loader=self.__class__.__name__)
name, [container], context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node_name = container["instance_node"]
node = rt.GetNodeByName(node_name)
instance_name, _ = node_name.split("_")
container = rt.GetNodeByName(instance_name)
for child in container.Children:
rt.Delete(child)
node = rt.getNodeByName(node_name)
namespace, name = get_namespace(node_name)
sub_node_name = f"{namespace}:{name}_{self.postfix}"
inst_container = rt.getNodeByName(sub_node_name)
rt.Select(inst_container.Children)
transform_data = object_transform_set(inst_container.Children)
for prev_obj in rt.selection:
if rt.isValidNode(prev_obj):
rt.Delete(prev_obj)
rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp')
# get current selection
for selection in rt.GetCurrentSelection():
selection.Parent = container
selections = rt.GetCurrentSelection()
update_custom_attribute_data(inst_container, selections)
for selection in selections:
selection.Parent = inst_container
selection.name = f"{namespace}:{selection.name}"
selection.pos = transform_data[
f"{selection.name}.transform"]
selection.scale = transform_data[
f"{selection.name}.scale"]
with maintained_selection():
rt.Select(node)

View file

@ -1,8 +1,16 @@
import os
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import (
unique_namespace,
get_namespace,
object_transform_set
)
from openpype.hosts.max.api.lib import maintained_selection
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data
)
from openpype.pipeline import get_representation_path, load
@ -15,6 +23,7 @@ class ModelUSDLoader(load.LoaderPlugin):
order = -10
icon = "code-fork"
color = "orange"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -30,11 +39,24 @@ class ModelUSDLoader(load.LoaderPlugin):
rt.LogLevel = rt.Name("info")
rt.USDImporter.importFile(filepath,
importOptions=import_options)
namespace = unique_namespace(
name + "_",
suffix="_",
)
asset = rt.GetNodeByName(name)
import_custom_attribute_data(asset, asset.Children)
for usd_asset in asset.Children:
usd_asset.name = f"{namespace}:{usd_asset.name}"
asset_name = f"{namespace}:{name}_{self.postfix}"
asset.name = asset_name
# need to get the correct container after renamed
asset = rt.GetNodeByName(asset_name)
return containerise(
name, [asset], context, loader=self.__class__.__name__)
name, [asset], context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
@ -42,11 +64,16 @@ class ModelUSDLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node_name = container["instance_node"]
node = rt.GetNodeByName(node_name)
namespace, name = get_namespace(node_name)
sub_node_name = f"{namespace}:{name}_{self.postfix}"
transform_data = None
for n in node.Children:
for r in n.Children:
rt.Delete(r)
rt.Select(n.Children)
transform_data = object_transform_set(n.Children)
for prev_usd_asset in rt.selection:
if rt.isValidNode(prev_usd_asset):
rt.Delete(prev_usd_asset)
rt.Delete(n)
instance_name, _ = node_name.split("_")
import_options = rt.USDImporter.CreateOptions()
base_filename = os.path.basename(path)
@ -55,11 +82,20 @@ class ModelUSDLoader(load.LoaderPlugin):
rt.LogPath = log_filepath
rt.LogLevel = rt.Name("info")
rt.USDImporter.importFile(path,
importOptions=import_options)
rt.USDImporter.importFile(
path, importOptions=import_options)
asset = rt.GetNodeByName(instance_name)
asset = rt.GetNodeByName(name)
asset.Parent = node
import_custom_attribute_data(asset, asset.Children)
for children in asset.Children:
children.name = f"{namespace}:{children.name}"
children.pos = transform_data[
f"{children.name}.transform"]
children.scale = transform_data[
f"{children.name}.scale"]
asset.name = sub_node_name
with maintained_selection():
rt.Select(node)

View file

@ -7,7 +7,12 @@ Because of limited api, alembics can be only loaded, but not easily updated.
import os
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api import lib, maintained_selection
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.lib import unique_namespace
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
class AbcLoader(load.LoaderPlugin):
@ -19,6 +24,7 @@ class AbcLoader(load.LoaderPlugin):
order = -10
icon = "code-fork"
color = "orange"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -33,7 +39,7 @@ class AbcLoader(load.LoaderPlugin):
}
rt.AlembicImport.ImportToRoot = False
rt.importFile(file_path, rt.name("noPrompt"))
rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport)
abc_after = {
c
@ -48,13 +54,27 @@ class AbcLoader(load.LoaderPlugin):
self.log.error("Something failed when loading.")
abc_container = abc_containers.pop()
for abc in rt.GetCurrentSelection():
selections = rt.GetCurrentSelection()
import_custom_attribute_data(
abc_container, abc_container.Children)
for abc in selections:
for cam_shape in abc.Children:
cam_shape.playbackType = 2
namespace = unique_namespace(
name + "_",
suffix="_",
)
for abc_object in abc_container.Children:
abc_object.name = f"{namespace}:{abc_object.name}"
# rename the abc container with namespace
abc_container_name = f"{namespace}:{name}_{self.postfix}"
abc_container.name = abc_container_name
return containerise(
name, [abc_container], context, loader=self.__class__.__name__
name, [abc_container], context,
namespace, loader=self.__class__.__name__
)
def update(self, container, representation):
@ -63,28 +83,23 @@ class AbcLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node = rt.GetNodeByName(container["instance_node"])
alembic_objects = self.get_container_children(node, "AlembicObject")
for alembic_object in alembic_objects:
alembic_object.source = path
lib.imprint(
container["instance_node"],
{"representation": str(representation["_id"])},
)
with maintained_selection():
rt.Select(node.Children)
for alembic in rt.Selection:
abc = rt.GetNodeByName(alembic.name)
update_custom_attribute_data(abc, abc.Children)
rt.Select(abc.Children)
for abc_con in rt.Selection:
container = rt.GetNodeByName(abc_con.name)
container.source = path
rt.Select(container.Children)
for abc_obj in rt.Selection:
alembic_obj = rt.GetNodeByName(abc_obj.name)
alembic_obj.source = path
for abc_con in abc.Children:
abc_con.source = path
rt.Select(abc_con.Children)
for abc_obj in abc_con.Children:
abc_obj.source = path
lib.imprint(
container["instance_node"],
{"representation": str(representation["_id"])},
)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -1,7 +1,14 @@
import os
from openpype.hosts.max.api import lib, maintained_selection
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.lib import (
unique_namespace, get_namespace
)
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.pipeline import get_representation_path, load
@ -13,6 +20,7 @@ class PointCloudLoader(load.LoaderPlugin):
order = -8
icon = "code-fork"
color = "green"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
"""load point cloud by tyCache"""
@ -22,10 +30,19 @@ class PointCloudLoader(load.LoaderPlugin):
obj = rt.tyCache()
obj.filename = filepath
prt_container = rt.GetNodeByName(obj.name)
namespace = unique_namespace(
name + "_",
suffix="_",
)
prt_container = rt.Container(
name=f"{namespace}:{name}_{self.postfix}")
import_custom_attribute_data(prt_container, [obj])
obj.Parent = prt_container
obj.name = f"{namespace}:{obj.name}"
return containerise(
name, [prt_container], context, loader=self.__class__.__name__)
name, [prt_container], context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
"""update the container"""
@ -33,15 +50,18 @@ class PointCloudLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node = rt.GetNodeByName(container["instance_node"])
namespace, name = get_namespace(container["instance_node"])
sub_node_name = f"{namespace}:{name}_{self.postfix}"
inst_container = rt.getNodeByName(sub_node_name)
update_custom_attribute_data(
inst_container, inst_container.Children)
with maintained_selection():
rt.Select(node.Children)
for prt in rt.Selection:
prt_object = rt.GetNodeByName(prt.name)
prt_object.filename = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
for prt in inst_container.Children:
prt.filename = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def switch(self, container, representation):
self.update(container, representation)

View file

@ -5,8 +5,15 @@ from openpype.pipeline import (
load,
get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api.pipeline import (
containerise,
import_custom_attribute_data,
update_custom_attribute_data
)
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import (
unique_namespace, get_namespace
)
class RedshiftProxyLoader(load.LoaderPlugin):
@ -18,6 +25,7 @@ class RedshiftProxyLoader(load.LoaderPlugin):
order = -9
icon = "code-fork"
color = "white"
postfix = "param"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
@ -30,24 +38,32 @@ class RedshiftProxyLoader(load.LoaderPlugin):
if collections:
rs_proxy.is_sequence = True
container = rt.container()
container.name = name
namespace = unique_namespace(
name + "_",
suffix="_",
)
container = rt.Container(
name=f"{namespace}:{name}_{self.postfix}")
rs_proxy.Parent = container
asset = rt.getNodeByName(name)
rs_proxy.name = f"{namespace}:{rs_proxy.name}"
import_custom_attribute_data(container, [rs_proxy])
return containerise(
name, [asset], context, loader=self.__class__.__name__)
name, [container], context,
namespace, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
for children in node.Children:
children_node = rt.getNodeByName(children.name)
for proxy in children_node.Children:
proxy.file = path
namespace, name = get_namespace(container["instance_node"])
sub_node_name = f"{namespace}:{name}_{self.postfix}"
inst_container = rt.getNodeByName(sub_node_name)
update_custom_attribute_data(
inst_container, inst_container.Children)
for proxy in inst_container.Children:
proxy.file = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])

View file

@ -34,6 +34,9 @@ class CollectRender(pyblish.api.InstancePlugin):
aovs = RenderProducts().get_aovs(instance.name)
files_by_aov.update(aovs)
camera = rt.viewport.GetCamera()
instance.data["cameras"] = [camera.name] if camera else None # noqa
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["files"] = list()

View file

@ -13,7 +13,6 @@ class ValidateMaxContents(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
families = ["camera",
"maxScene",
"maxrender",
"review"]
hosts = ["max"]
label = "Max Scene Contents"

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin)
from openpype.pipeline.publish import RepairAction
from openpype.hosts.max.api.lib import get_current_renderer
from pymxs import runtime as rt
class ValidateRenderableCamera(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validates Renderable Camera
Check if the renderable camera used for rendering
"""
order = pyblish.api.ValidatorOrder
families = ["maxrender"]
hosts = ["max"]
label = "Renderable Camera"
optional = True
actions = [RepairAction]
def process(self, instance):
if not self.is_active(instance.data):
return
if not instance.data["cameras"]:
raise PublishValidationError(
"No renderable Camera found in scene."
)
@classmethod
def repair(cls, instance):
rt.viewport.setType(rt.Name("view_camera"))
camera = rt.viewport.GetCamera()
cls.log.info(f"Camera {camera} set as renderable camera")
renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0]
if renderer == "Arnold":
arv = rt.MAXToAOps.ArnoldRenderView()
arv.setOption("Camera", str(camera))
arv.close()
instance.data["cameras"] = [camera.name]

View file

@ -260,7 +260,7 @@ class MayaCreator(NewCreator, MayaCreatorBase):
default=True)
]
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
"""Method called on initialization of plugin to apply settings."""
settings_name = self.settings_name

View file

@ -81,10 +81,8 @@ class CreateAnimation(plugin.MayaHiddenCreator):
return defs
def apply_settings(self, project_settings, system_settings):
super(CreateAnimation, self).apply_settings(
project_settings, system_settings
)
def apply_settings(self, project_settings):
super(CreateAnimation, self).apply_settings(project_settings)
# Hardcoding creator to be enabled due to existing settings would
# disable the creator causing the creator plugin to not be
# discoverable.

View file

@ -34,7 +34,7 @@ class CreateRenderlayer(plugin.RenderlayerCreator):
render_settings = {}
@classmethod
def apply_settings(cls, project_settings, system_settings):
def apply_settings(cls, project_settings):
cls.render_settings = project_settings["maya"]["RenderSettings"]
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -21,7 +21,7 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator):
# Defined in settings
joint_hints = set()
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
"""Apply project settings to creator"""
settings = (
project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"]

View file

@ -16,7 +16,7 @@ class CreateUnrealStaticMesh(plugin.MayaCreator):
# Defined in settings
collision_prefixes = []
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
"""Apply project settings to creator"""
settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"]
self.collision_prefixes = settings["collision_prefixes"]

View file

@ -22,7 +22,7 @@ class CreateVRayScene(plugin.RenderlayerCreator):
singleton_node_name = "vraysceneMain"
@classmethod
def apply_settings(cls, project_settings, system_settings):
def apply_settings(cls, project_settings):
cls.render_settings = project_settings["maya"]["RenderSettings"]
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -249,7 +249,6 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin):
Authenticate with Muster, collect all data, prepare path for post
render publish job and submit job to farm.
"""
instance.data["toBeRenderedOn"] = "muster"
# setup muster environment
self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL")

View file

@ -1,60 +0,0 @@
from maya import cmds
import pyblish.api
from openpype.pipeline.publish import (
ValidateContentsOrder, PublishValidationError, RepairAction
)
from openpype.pipeline import discover_legacy_creator_plugins
from openpype.hosts.maya.api.lib import imprint
class ValidateInstanceAttributes(pyblish.api.InstancePlugin):
"""Validate Instance Attributes.
New attributes can be introduced as new features come in. Old instances
will need to be updated with these attributes for the documentation to make
sense, and users do not have to recreate the instances.
"""
order = ValidateContentsOrder
hosts = ["maya"]
families = ["*"]
label = "Instance Attributes"
plugins_by_family = {
p.family: p for p in discover_legacy_creator_plugins()
}
actions = [RepairAction]
@classmethod
def get_missing_attributes(self, instance):
plugin = self.plugins_by_family[instance.data["family"]]
subset = instance.data["subset"]
asset = instance.data["asset"]
objset = instance.data["objset"]
missing_attributes = {}
for key, value in plugin(subset, asset).data.items():
if not cmds.objExists("{}.{}".format(objset, key)):
missing_attributes[key] = value
return missing_attributes
def process(self, instance):
objset = instance.data.get("objset")
if objset is None:
self.log.debug(
"Skipping {} because no objectset found.".format(instance)
)
return
missing_attributes = self.get_missing_attributes(instance)
if missing_attributes:
raise PublishValidationError(
"Missing attributes on {}:\n{}".format(
objset, missing_attributes
)
)
@classmethod
def repair(cls, instance):
imprint(instance.data["objset"], cls.get_missing_attributes(instance))

View file

@ -3,94 +3,19 @@
from __future__ import absolute_import
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder, PublishValidationError
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
from maya import cmds
class SelectInvalidInstances(pyblish.api.Action):
"""Select invalid instances in Outliner."""
label = "Select Instances"
icon = "briefcase"
on = "failed"
def process(self, context, plugin):
"""Process invalid validators and select invalid instances."""
# Get the errored instances
failed = []
for result in context.data["results"]:
if (
result["error"] is None
or result["instance"] is None
or result["instance"] in failed
or result["plugin"] != plugin
):
continue
failed.append(result["instance"])
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
if instances:
self.log.info(
"Selecting invalid nodes: %s" % ", ".join(
[str(x) for x in instances]
)
)
self.select(instances)
else:
self.log.info("No invalid nodes found.")
self.deselect()
def select(self, instances):
cmds.select(instances, replace=True, noExpand=True)
def deselect(self):
cmds.select(deselect=True)
class RepairSelectInvalidInstances(pyblish.api.Action):
"""Repair the instance asset."""
label = "Repair"
icon = "wrench"
on = "failed"
def process(self, context, plugin):
# Get the errored instances
failed = []
for result in context.data["results"]:
if result["error"] is None:
continue
if result["instance"] is None:
continue
if result["instance"] in failed:
continue
if result["plugin"] != plugin:
continue
failed.append(result["instance"])
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
context_asset = context.data["assetEntity"]["name"]
for instance in instances:
self.set_attribute(instance, context_asset)
def set_attribute(self, instance, context_asset):
cmds.setAttr(
instance.data.get("name") + ".asset",
context_asset,
type="string"
)
class ValidateInstanceInContext(pyblish.api.InstancePlugin):
class ValidateInstanceInContext(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validator to check if instance asset match context asset.
When working in per-shot style you always publish data in context of
@ -104,11 +29,49 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin):
label = "Instance in same Context"
optional = True
hosts = ["maya"]
actions = [SelectInvalidInstances, RepairSelectInvalidInstances]
actions = [
openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction
]
def process(self, instance):
if not self.is_active(instance.data):
return
asset = instance.data.get("asset")
context_asset = instance.context.data["assetEntity"]["name"]
msg = "{} has asset {}".format(instance.name, asset)
context_asset = self.get_context_asset(instance)
if asset != context_asset:
raise PublishValidationError(msg)
raise PublishValidationError(
message=(
"Instance '{}' publishes to different asset than current "
"context: {}. Current context: {}".format(
instance.name, asset, context_asset
)
),
description=(
"## Publishing to a different asset\n"
"There are publish instances present which are publishing "
"into a different asset than your current context.\n\n"
"Usually this is not what you want but there can be cases "
"where you might want to publish into another asset or "
"shot. If that's the case you can disable the validation "
"on the instance to ignore it."
)
)
@classmethod
def get_invalid(cls, instance):
return [instance.data["instance_node"]]
@classmethod
def repair(cls, instance):
context_asset = cls.get_context_asset(instance)
instance_node = instance.data["instance_node"]
cmds.setAttr(
"{}.asset".format(instance_node),
context_asset,
type="string"
)
@staticmethod
def get_context_asset(instance):
return instance.context.data["assetEntity"]["name"]

View file

@ -4,6 +4,8 @@ from maya import cmds
import pyblish.api
from openpype.hosts.maya.api.lib import pairwise
from openpype.hosts.maya.api.action import SelectInvalidAction
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError
@ -19,31 +21,33 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
hosts = ['maya']
families = ["workfile"]
label = "Plug-in Path Attributes"
actions = [SelectInvalidAction]
def get_invalid(self, instance):
# Attributes are defined in project settings
attribute = []
@classmethod
def get_invalid(cls, instance):
invalid = list()
# get the project setting
validate_path = (
instance.context.data["project_settings"]["maya"]["publish"]
)
file_attr = validate_path["ValidatePluginPathAttributes"]["attribute"]
file_attr = cls.attribute
if not file_attr:
return invalid
# get the nodes and file attributes
for node, attr in file_attr.items():
# check the related nodes
targets = cmds.ls(type=node)
# Consider only valid node types to avoid "Unknown object type" warning
all_node_types = set(cmds.allNodeTypes())
node_types = [key for key in file_attr.keys() if key in all_node_types]
for target in targets:
# get the filepath
file_attr = "{}.{}".format(target, attr)
filepath = cmds.getAttr(file_attr)
for node, node_type in pairwise(cmds.ls(type=node_types,
showType=True)):
# get the filepath
file_attr = "{}.{}".format(node, file_attr[node_type])
filepath = cmds.getAttr(file_attr)
if filepath and not os.path.exists(filepath):
self.log.error("File {0} not exists".format(filepath)) # noqa
invalid.append(target)
if filepath and not os.path.exists(filepath):
cls.log.error("{} '{}' uses non-existing filepath: {}"
.format(node_type, node, filepath))
invalid.append(node)
return invalid
@ -51,5 +55,16 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
"""Process all directories Set as Filenames in Non-Maya Nodes"""
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Non-existent Path "
"found: {0}".format(invalid))
raise PublishValidationError(
title="Plug-in Path Attributes",
message="Non-existent filepath found on nodes: {}".format(
", ".join(invalid)
),
description=(
"## Plug-in nodes use invalid filepaths\n"
"The workfile contains nodes from plug-ins that use "
"filepaths which do not exist.\n\n"
"Please make sure their filepaths are correct and the "
"files exist on disk."
)
)

View file

@ -7,6 +7,7 @@ from openpype.hosts.maya.api import lib
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
PublishValidationError
)
@ -67,5 +68,30 @@ class ValidateShapeZero(pyblish.api.Validator):
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Shapes found with non-zero component tweaks: "
"{0}".format(invalid))
raise PublishValidationError(
title="Shape Component Tweaks",
message="Shapes found with non-zero component tweaks: '{}'"
"".format(", ".join(invalid)),
description=(
"## Shapes found with component tweaks\n"
"Shapes were detected that have component tweaks on their "
"components. Please remove the component tweaks to "
"continue.\n\n"
"### Repair\n"
"The repair action will try to *freeze* the component "
"tweaks into the shapes, which is usually the correct fix "
"if the mesh has no construction history (= has its "
"history deleted)."),
detail=(
"Maya allows to store component tweaks within shape nodes "
"which are applied between its `inMesh` and `outMesh` "
"connections resulting in the output of a shape node "
"differing from the input. We usually want to avoid this "
"for published meshes (in particular for Maya scenes) as "
"it can have unintended results when using these meshes "
"as intermediate meshes since it applies positional "
"differences without being visible edits in the node "
"graph.\n\n"
"These tweaks are traditionally stored in the `.pnts` "
"attribute of shapes.")
)

View file

@ -2041,6 +2041,7 @@ class WorkfileSettings(object):
)
workfile_settings = imageio_host["workfile"]
viewer_process_settings = imageio_host["viewer"]["viewerProcess"]
if not config_data:
# TODO: backward compatibility for old projects - remove later
@ -2091,6 +2092,15 @@ class WorkfileSettings(object):
workfile_settings.pop("colorManagement", None)
workfile_settings.pop("OCIO_config", None)
# get monitor lut from settings respecting Nuke version differences
monitor_lut = workfile_settings.pop("monitorLut", None)
monitor_lut_data = self._get_monitor_settings(
viewer_process_settings, monitor_lut)
# set monitor related knobs luts (MonitorOut, Thumbnails)
for knob, value_ in monitor_lut_data.items():
workfile_settings[knob] = value_
# then set the rest
for knob, value_ in workfile_settings.items():
# skip unfilled ocio config path
@ -2107,8 +2117,9 @@ class WorkfileSettings(object):
# set ocio config path
if config_data:
config_path = config_data["path"].replace("\\", "/")
log.info("OCIO config path found: `{}`".format(
config_data["path"]))
config_path))
# check if there's a mismatch between environment and settings
correct_settings = self._is_settings_matching_environment(
@ -2118,6 +2129,40 @@ class WorkfileSettings(object):
if correct_settings:
self._set_ocio_config_path_to_workfile(config_data)
def _get_monitor_settings(self, viewer_lut, monitor_lut):
""" Get monitor settings from viewer and monitor lut
Args:
viewer_lut (str): viewer lut string
monitor_lut (str): monitor lut string
Returns:
dict: monitor settings
"""
output_data = {}
m_display, m_viewer = get_viewer_config_from_string(monitor_lut)
v_display, v_viewer = get_viewer_config_from_string(viewer_lut)
# set monitor lut differently for nuke version 14
if nuke.NUKE_VERSION_MAJOR >= 14:
output_data["monitorOutLUT"] = create_viewer_profile_string(
m_viewer, m_display, path_like=False)
# monitorLut=thumbnails - viewerProcess makes more sense
output_data["monitorLut"] = create_viewer_profile_string(
v_viewer, v_display, path_like=False)
if nuke.NUKE_VERSION_MAJOR == 13:
output_data["monitorOutLUT"] = create_viewer_profile_string(
m_viewer, m_display, path_like=False)
# monitorLut=thumbnails - viewerProcess makes more sense
output_data["monitorLut"] = create_viewer_profile_string(
v_viewer, v_display, path_like=True)
if nuke.NUKE_VERSION_MAJOR <= 12:
output_data["monitorLut"] = create_viewer_profile_string(
m_viewer, m_display, path_like=True)
return output_data
def _is_settings_matching_environment(self, config_data):
""" Check if OCIO config path is different from environment
@ -2177,6 +2222,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
"""
# replace path with env var if possible
ocio_path = self._replace_ocio_path_with_env_var(config_data)
ocio_path = ocio_path.replace("\\", "/")
log.info("Setting OCIO config path to: `{}`".format(
ocio_path))
@ -2232,7 +2278,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
Returns:
str: OCIO config path with environment variable TCL expression
"""
config_path = config_data["path"]
config_path = config_data["path"].replace("\\", "/")
config_template = config_data["template"]
included_vars = self._get_included_vars(config_template)
@ -3320,11 +3366,11 @@ def get_viewer_config_from_string(input_string):
display = split[0]
elif "(" in viewer:
pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]"
result = re.findall(pattern, viewer)
result_ = re.findall(pattern, viewer)
try:
result = result.pop()
display = str(result[1]).rstrip()
viewer = str(result[0]).rstrip()
result_ = result_.pop()
display = str(result_[1]).rstrip()
viewer = str(result_[0]).rstrip()
except IndexError:
raise IndexError((
"Viewer Input string is not correct. "
@ -3332,3 +3378,22 @@ def get_viewer_config_from_string(input_string):
).format(input_string))
return (display, viewer)
def create_viewer_profile_string(viewer, display=None, path_like=False):
"""Convert viewer and display to string
Args:
viewer (str): viewer name
display (Optional[str]): display name
path_like (Optional[bool]): if True, return path like string
Returns:
str: viewer config string
"""
if not display:
return viewer
if path_like:
return "{}/{}".format(display, viewer)
return "{} ({})".format(viewer, display)

View file

@ -379,11 +379,7 @@ class NukeWriteCreator(NukeCreator):
sys.exc_info()[2]
)
def apply_settings(
self,
project_settings,
system_settings
):
def apply_settings(self, project_settings):
"""Method called on initialization of plugin to apply settings."""
# plugin settings

View file

@ -14,27 +14,26 @@ class RepairActionBase(pyblish.api.Action):
# Get the errored instances
return get_errored_instances_from_context(context, plugin=plugin)
def repair_knob(self, instances, state):
def repair_knob(self, context, instances, state):
create_context = context.data["create_context"]
for instance in instances:
node = instance.data["transientData"]["node"]
files_remove = [os.path.join(instance.data["outputDir"], f)
for r in instance.data.get("representations", [])
for f in r.get("files", [])
]
self.log.info("Files to be removed: {}".format(files_remove))
for f in files_remove:
os.remove(f)
self.log.debug("removing file: {}".format(f))
node["render"].setValue(state)
# Reset the render knob
instance_id = instance.data.get("instance_id")
created_instance = create_context.get_instance_by_id(
instance_id
)
created_instance.creator_attributes["render_target"] = state
self.log.info("Rendering toggled to `{}`".format(state))
create_context.save_changes()
class RepairCollectionActionToLocal(RepairActionBase):
label = "Repair - rerender with \"Local\""
def process(self, context, plugin):
instances = self.get_instance(context, plugin)
self.repair_knob(instances, "Local")
self.repair_knob(context, instances, "local")
class RepairCollectionActionToFarm(RepairActionBase):
@ -42,7 +41,7 @@ class RepairCollectionActionToFarm(RepairActionBase):
def process(self, context, plugin):
instances = self.get_instance(context, plugin)
self.repair_knob(instances, "On farm")
self.repair_knob(context, instances, "farm")
class ValidateRenderedFrames(pyblish.api.InstancePlugin):

View file

@ -1,3 +1,5 @@
from collections import defaultdict
import pyblish.api
from openpype.pipeline.publish import get_errored_instances_from_context
from openpype.hosts.nuke.api.lib import (
@ -87,6 +89,11 @@ class ValidateNukeWriteNode(
correct_data
))
# Collect key values of same type in a list.
values_by_name = defaultdict(list)
for knob_data in correct_data["knobs"]:
values_by_name[knob_data["name"]].append(knob_data["value"])
for knob_data in correct_data["knobs"]:
knob_type = knob_data["type"]
self.log.debug("__ knob_type: {}".format(
@ -105,28 +112,33 @@ class ValidateNukeWriteNode(
)
key = knob_data["name"]
value = knob_data["value"]
values = values_by_name[key]
node_value = write_node[key].value()
# fix type differences
if type(node_value) in (int, float):
try:
if isinstance(value, list):
value = color_gui_to_int(value)
else:
value = float(value)
node_value = float(node_value)
except ValueError:
value = str(value)
else:
value = str(value)
node_value = str(node_value)
fixed_values = []
for value in values:
if type(node_value) in (int, float):
try:
self.log.debug("__ key: {} | value: {}".format(
key, value
if isinstance(value, list):
value = color_gui_to_int(value)
else:
value = float(value)
node_value = float(node_value)
except ValueError:
value = str(value)
else:
value = str(value)
node_value = str(node_value)
fixed_values.append(value)
self.log.debug("__ key: {} | values: {}".format(
key, fixed_values
))
if (
node_value != value
node_value not in fixed_values
and key != "file"
and key != "tile_color"
):

View file

@ -98,7 +98,7 @@ class AutoImageCreator(PSAutoCreator):
)
]
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["AutoImageCreator"]
)

View file

@ -171,7 +171,7 @@ class ImageCreator(Creator):
)
]
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["ImageCreator"]
)

View file

@ -18,7 +18,7 @@ class ReviewCreator(PSAutoCreator):
it will get recreated in next publish either way).
"""
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["ReviewCreator"]
)

View file

@ -19,7 +19,7 @@ class WorkfileCreator(PSAutoCreator):
in next publish automatically).
"""
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["WorkfileCreator"]
)

View file

@ -36,7 +36,7 @@ class BatchMovieCreator(TrayPublishCreator):
# Position batch creator after simple creators
order = 110
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
creator_settings = (
project_settings["traypublisher"]["create"]["BatchMovieCreator"]
)

View file

@ -139,7 +139,7 @@ class CreateRenderlayer(TVPaintCreator):
# - Mark by default instance for review
mark_for_review = True
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["tvpaint"]["create"]["create_render_layer"]
)
@ -387,7 +387,7 @@ class CreateRenderPass(TVPaintCreator):
# Settings
mark_for_review = True
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["tvpaint"]["create"]["create_render_pass"]
)
@ -690,7 +690,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator):
group_idx_offset = 10
group_idx_padding = 3
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings
["tvpaint"]
@ -1029,7 +1029,7 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator):
mark_for_review = True
active_on_create = False
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["tvpaint"]["create"]["create_render_scene"]
)

View file

@ -12,7 +12,7 @@ class TVPaintReviewCreator(TVPaintAutoCreator):
# Settings
active_on_create = True
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["tvpaint"]["create"]["create_review"]
)

View file

@ -9,7 +9,7 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator):
label = "Workfile"
icon = "fa.file-o"
def apply_settings(self, project_settings, system_settings):
def apply_settings(self, project_settings):
plugin_settings = (
project_settings["tvpaint"]["create"]["create_workfile"]
)

View file

@ -2,6 +2,8 @@
"""Hook to launch Unreal and prepare projects."""
import os
import copy
import shutil
import tempfile
from pathlib import Path
from qtpy import QtCore
@ -224,10 +226,24 @@ class UnrealPrelaunchHook(PreLaunchHook):
project_file = project_path / unreal_project_filename
if not project_file.is_file():
self.exec_ue_project_gen(engine_version,
unreal_project_name,
engine_path,
project_path)
with tempfile.TemporaryDirectory() as temp_dir:
self.exec_ue_project_gen(engine_version,
unreal_project_name,
engine_path,
Path(temp_dir))
try:
self.log.info((
f"Moving from {temp_dir} to "
f"{project_path.as_posix()}"
))
shutil.copytree(
temp_dir, project_path, dirs_exist_ok=True)
except shutil.Error as e:
raise ApplicationLaunchFailed((
f"{self.signature} Cannot copy directory {temp_dir} "
f"to {project_path.as_posix()} - {e}"
)) from e
self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version
# Append project file to launch arguments

View file

@ -280,13 +280,14 @@ class BatchPublishEndpoint(WebpublishApiEndpoint):
for key, value in add_args.items():
# Skip key values where value is None
if value is not None:
args.append("--{}".format(key))
# Extend list into arguments (targets can be a list)
if isinstance(value, (tuple, list)):
args.extend(value)
else:
args.append(value)
if value is None:
continue
arg_key = "--{}".format(key)
if not isinstance(value, (tuple, list)):
value = [value]
for item in value:
args += [arg_key, item]
log.info("args:: {}".format(args))
if add_to_queue:

View file

@ -424,17 +424,25 @@ class TextDef(AbstractAttrDef):
class EnumDef(AbstractAttrDef):
"""Enumeration of single item from items.
"""Enumeration of items.
Enumeration of single item from items. Or list of items if multiselection
is enabled.
Args:
items: Items definition that can be converted using
'prepare_enum_items'.
default: Default value. Must be one key(value) from passed items.
items (Union[list[str], list[dict[str, Any]]): Items definition that
can be converted using 'prepare_enum_items'.
default (Optional[Any]): Default value. Must be one key(value) from
passed items or list of values for multiselection.
multiselection (Optional[bool]): If True, multiselection is allowed.
Output is list of selected items.
"""
type = "enum"
def __init__(self, key, items, default=None, **kwargs):
def __init__(
self, key, items, default=None, multiselection=False, **kwargs
):
if not items:
raise ValueError((
"Empty 'items' value. {} must have"
@ -443,30 +451,44 @@ class EnumDef(AbstractAttrDef):
items = self.prepare_enum_items(items)
item_values = [item["value"] for item in items]
if default not in item_values:
for value in item_values:
default = value
break
item_values_set = set(item_values)
if multiselection:
if default is None:
default = []
default = list(item_values_set.intersection(default))
elif default not in item_values:
default = next(iter(item_values), None)
super(EnumDef, self).__init__(key, default=default, **kwargs)
self.items = items
self._item_values = set(item_values)
self._item_values = item_values_set
self.multiselection = multiselection
def __eq__(self, other):
if not super(EnumDef, self).__eq__(other):
return False
return self.items == other.items
return (
self.items == other.items
and self.multiselection == other.multiselection
)
def convert_value(self, value):
if value in self._item_values:
return value
return self.default
if not self.multiselection:
if value in self._item_values:
return value
return self.default
if value is None:
return copy.deepcopy(self.default)
return list(self._item_values.intersection(value))
def serialize(self):
data = super(EnumDef, self).serialize()
data["items"] = copy.deepcopy(self.items)
data["multiselection"] = self.multiselection
return data
@staticmethod

View file

@ -270,8 +270,8 @@ def is_func_signature_supported(func, *args, **kwargs):
Args:
func (function): A function where the signature should be tested.
*args (tuple[Any]): Positional arguments for function signature.
**kwargs (dict[str, Any]): Keyword arguments for function signature.
*args (Any): Positional arguments for function signature.
**kwargs (Any): Keyword arguments for function signature.
Returns:
bool: Function can pass in arguments.

View file

@ -373,10 +373,12 @@ def _load_ayon_addons(openpype_modules, modules_key, log):
addons_info = _get_ayon_addons_information()
if not addons_info:
return v3_addons_to_skip
addons_dir = os.path.join(
appdirs.user_data_dir("AYON", "Ynput"),
"addons"
)
addons_dir = os.environ.get("AYON_ADDONS_DIR")
if not addons_dir:
addons_dir = os.path.join(
appdirs.user_data_dir("AYON", "Ynput"),
"addons"
)
if not os.path.exists(addons_dir):
log.warning("Addons directory does not exists. Path \"{}\"".format(
addons_dir

View file

@ -27,7 +27,7 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin):
deadline_job_delay = "00:00:08:00"
def process(self, instance):
instance.data["toBeRenderedOn"] = "deadline"
context = instance.context
# get default deadline webservice url from deadline module

View file

@ -265,7 +265,7 @@ class HarmonySubmitDeadline(
job_info.SecondaryPool = self._instance.data.get("secondaryPool")
job_info.ChunkSize = self.chunk_size
batch_name = os.path.basename(self._instance.data["source"])
if is_in_tests:
if is_in_tests():
batch_name += datetime.now().strftime("%d%m%Y%H%M%S")
job_info.BatchName = batch_name
job_info.Department = self.department

View file

@ -141,4 +141,3 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
# Store output dir for unified publisher (filesequence)
output_dir = os.path.dirname(instance.data["files"][0])
instance.data["outputDir"] = output_dir
instance.data["toBeRenderedOn"] = "deadline"

View file

@ -176,7 +176,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
first_file = next(self._iter_expected_files(files))
output_dir = os.path.dirname(first_file)
instance.data["outputDir"] = output_dir
instance.data["toBeRenderedOn"] = "deadline"
filename = os.path.basename(filepath)
@ -238,7 +237,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
if renderer == "Redshift_Renderer":
plugin_data["redshift_SeparateAovFiles"] = instance.data.get(
"separateAovFiles")
if instance.data["cameras"]:
plugin_info["Camera0"] = None
plugin_info["Camera"] = instance.data["cameras"][0]
plugin_info["Camera1"] = instance.data["cameras"][0]
self.log.debug("plugin data:{}".format(plugin_data))
plugin_info.update(plugin_data)

View file

@ -300,7 +300,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
first_file = next(iter_expected_files(expected_files))
output_dir = os.path.dirname(first_file)
instance.data["outputDir"] = output_dir
instance.data["toBeRenderedOn"] = "deadline"
# Patch workfile (only when use_published is enabled)
if self.use_published:
@ -335,12 +334,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
payload = self._get_vray_render_payload(payload_data)
elif "assscene" in instance.data["families"]:
self.log.debug("Submitting Arnold .ass standalone render..")
ass_export_payload = self._get_arnold_export_payload(payload_data)
export_job = self.submit(ass_export_payload)
payload = self._get_arnold_render_payload(payload_data)
else:
self.log.debug("Submitting MayaBatch render..")
payload = self._get_maya_payload(payload_data)
@ -636,53 +629,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
return job_info, attr.asdict(plugin_info)
def _get_arnold_export_payload(self, data):
try:
from openpype.scripts import export_maya_ass_job
except Exception:
raise AssertionError(
"Expected module 'export_maya_ass_job' to be available")
module_path = export_maya_ass_job.__file__
if module_path.endswith(".pyc"):
module_path = module_path[: -len(".pyc")] + ".py"
script = os.path.normpath(module_path)
job_info = copy.deepcopy(self.job_info)
job_info.Name = self._job_info_label("Export")
# Force a single frame Python job
job_info.Plugin = "Python"
job_info.Frames = 1
renderlayer = self._instance.data["setMembers"]
# add required env vars for the export script
envs = {
"AVALON_APP_NAME": os.environ.get("AVALON_APP_NAME"),
"OPENPYPE_ASS_EXPORT_RENDER_LAYER": renderlayer,
"OPENPYPE_ASS_EXPORT_SCENE_FILE": self.scene_path,
"OPENPYPE_ASS_EXPORT_OUTPUT": job_info.OutputFilename[0],
"OPENPYPE_ASS_EXPORT_START": int(self._instance.data["frameStartHandle"]), # noqa
"OPENPYPE_ASS_EXPORT_END": int(self._instance.data["frameEndHandle"]), # noqa
"OPENPYPE_ASS_EXPORT_STEP": 1
}
for key, value in envs.items():
if not value:
continue
job_info.EnvironmentKeyValue[key] = value
plugin_info = PythonPluginInfo(
ScriptFile=script,
Version="3.6",
Arguments="",
SingleFrameOnly="True"
)
return job_info, attr.asdict(plugin_info)
def _get_vray_render_payload(self, data):
# Job Info

View file

@ -97,7 +97,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
instance.data["suspend_publish"] = instance.data["attributeValues"][
"suspend_publish"]
instance.data["toBeRenderedOn"] = "deadline"
families = instance.data["families"]
node = instance.data["transientData"]["node"]

View file

@ -70,7 +70,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
# Update the representation expected files
self.log.info("Update range from actual job range "
"to frame list: {}".format(frame_list))
repre["files"] = sorted(job_expected_files)
# single item files must be string not list
repre["files"] = (sorted(job_expected_files)
if len(job_expected_files) > 1 else
list(job_expected_files)[0])
# Update the expected files
expected_files = job_expected_files

View file

@ -91,7 +91,13 @@ class AyonDeadlinePlugin(DeadlinePlugin):
# clean '\ ' for MacOS pasting
if platform.system().lower() == "darwin":
exe_list = exe_list.replace("\\ ", " ")
exe = FileUtils.SearchFileList(exe_list)
expanded_paths = []
for path in exe_list.split(";"):
if path.startswith("~"):
path = os.path.expanduser(path)
expanded_paths.append(path)
exe = FileUtils.SearchFileList(";".join(expanded_paths))
if exe == "":
self.FailRender(

View file

@ -547,7 +547,14 @@ def get_ayon_executable():
# clean '\ ' for MacOS pasting
if platform.system().lower() == "darwin":
exe_list = exe_list.replace("\\ ", " ")
return exe_list
# Expand user paths
expanded_paths = []
for path in exe_list.split(";"):
if path.startswith("~"):
path = os.path.expanduser(path)
expanded_paths.append(path)
return ";".join(expanded_paths)
def inject_render_job_id(deadlinePlugin):

View file

@ -77,4 +77,22 @@ CategoryOrder=0
Index=4
Label=Harmony 20 Render Executable
Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.
Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium
Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium
[Harmony_RenderExecutable_21]
Type=multilinemultifilename
Category=Render Executables
CategoryOrder=0
Index=4
Label=Harmony 21 Render Executable
Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.
Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 21 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_21/lnx86_64/bin/HarmonyPremium
[Harmony_RenderExecutable_22]
Type=multilinemultifilename
Category=Render Executables
CategoryOrder=0
Index=4
Label=Harmony 22 Render Executable
Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.
Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 22 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_22/lnx86_64/bin/HarmonyPremium

View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
from System import *
from System.Diagnostics import *
from System.IO import *

View file

@ -1774,7 +1774,7 @@ class CreateContext:
self.creator_discover_result = report
for creator_class in report.plugins:
if inspect.isabstract(creator_class):
self.log.info(
self.log.debug(
"Skipping abstract Creator {}".format(str(creator_class))
)
continue
@ -1804,6 +1804,7 @@ class CreateContext:
self,
self.headless
)
if not creator.enabled:
disabled_creators[creator_identifier] = creator
continue

View file

@ -1,16 +1,12 @@
import copy
import collections
from abc import (
ABCMeta,
abstractmethod,
abstractproperty
)
from abc import ABCMeta, abstractmethod
import six
from openpype.settings import get_system_settings, get_project_settings
from openpype.lib import Logger
from openpype.lib import Logger, is_func_signature_supported
from openpype.pipeline.plugin_discover import (
discover,
register_plugin,
@ -84,7 +80,8 @@ class SubsetConvertorPlugin(object):
def host(self):
return self._create_context.host
@abstractproperty
@property
@abstractmethod
def identifier(self):
"""Converted identifier.
@ -161,7 +158,6 @@ class BaseCreator:
Args:
project_settings (Dict[str, Any]): Project settings.
system_settings (Dict[str, Any]): System settings.
create_context (CreateContext): Context which initialized creator.
headless (bool): Running in headless mode.
"""
@ -208,10 +204,33 @@ class BaseCreator:
# - we may use UI inside processing this attribute should be checked
self.headless = headless
self.apply_settings(project_settings, system_settings)
expect_system_settings = False
if is_func_signature_supported(
self.apply_settings, project_settings
):
self.apply_settings(project_settings)
else:
expect_system_settings = True
# Backwards compatibility for system settings
self.apply_settings(project_settings, system_settings)
def apply_settings(self, project_settings, system_settings):
"""Method called on initialization of plugin to apply settings."""
init_overriden = self.__class__.__init__ is not BaseCreator.__init__
if init_overriden or expect_system_settings:
self.log.warning((
"WARNING: Source - Create plugin {}."
" System settings argument will not be passed to"
" '__init__' and 'apply_settings' methods in future versions"
" of OpenPype. Planned version to drop the support"
" is 3.16.6 or 3.17.0. Please contact Ynput core team if you"
" need to keep system settings."
).format(self.__class__.__name__))
def apply_settings(self, project_settings):
"""Method called on initialization of plugin to apply settings.
Args:
project_settings (dict[str, Any]): Project settings.
"""
pass
@ -224,7 +243,8 @@ class BaseCreator:
return self.family
@abstractproperty
@property
@abstractmethod
def family(self):
"""Family that plugin represents."""

View file

@ -75,7 +75,6 @@ class RenderInstance(object):
tilesY = attr.ib(default=0) # number of tiles in Y
# submit_publish_job
toBeRenderedOn = attr.ib(default=None)
deadlineSubmissionJob = attr.ib(default=None)
anatomyData = attr.ib(default=None)
outputDir = attr.ib(default=None)

View file

@ -952,6 +952,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True):
return file_path
def add_repre_files_for_cleanup(instance, repre):
""" Explicitly mark repre files to be deleted.
@ -960,7 +961,16 @@ def add_repre_files_for_cleanup(instance, repre):
"""
files = repre["files"]
staging_dir = repre.get("stagingDir")
if not staging_dir or instance.data.get("stagingDir_persistent"):
# first make sure representation level is not persistent
if (
not staging_dir
or repre.get("stagingDir_persistent")
):
return
# then look into instance level if it's not persistent
if instance.data.get("stagingDir_persistent"):
return
if isinstance(files, str):

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import pyblish.api
class CollectFarmTarget(pyblish.api.InstancePlugin):
"""Collects the render target for the instance
"""
order = pyblish.api.CollectorOrder + 0.499
label = "Collect Farm Target"
targets = ["local"]
def process(self, instance):
if not instance.data.get("farm"):
return
context = instance.context
farm_name = ""
op_modules = context.data.get("openPypeModules")
for farm_renderer in ["deadline", "royalrender", "muster"]:
op_module = op_modules.get(farm_renderer, False)
if op_module and op_module.enabled:
farm_name = farm_renderer
elif not op_module:
self.log.error("Cannot get OpenPype {0} module.".format(
farm_renderer))
if farm_name:
self.log.debug("Collected render target: {0}".format(farm_name))
instance.data["toBeRenderedOn"] = farm_name
else:
AssertionError("No OpenPype renderer module found")

View file

@ -8,6 +8,11 @@ from ayon_api import slugify_string
from ayon_api.entity_hub import EntityHub
from openpype import AYON_SERVER_ENABLED
from openpype.client import get_assets
from openpype.pipeline.template_data import (
get_asset_template_data,
get_task_template_data,
)
def _default_json_parse(value):
@ -31,6 +36,44 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
return
project_name = context.data["projectName"]
self._create_hierarchy(context, project_name)
self._fill_instance_entities(context, project_name)
def _fill_instance_entities(self, context, project_name):
instances_by_asset_name = collections.defaultdict(list)
for instance in context:
if instance.data.get("publish") is False:
continue
instance_entity = instance.data.get("assetEntity")
if instance_entity:
continue
# Skip if instance asset does not match
instance_asset_name = instance.data.get("asset")
instances_by_asset_name[instance_asset_name].append(instance)
project_doc = context.data["projectEntity"]
asset_docs = get_assets(
project_name, asset_names=instances_by_asset_name.keys()
)
asset_docs_by_name = {
asset_doc["name"]: asset_doc
for asset_doc in asset_docs
}
for asset_name, instances in instances_by_asset_name.items():
asset_doc = asset_docs_by_name[asset_name]
asset_data = get_asset_template_data(asset_doc, project_name)
for instance in instances:
task_name = instance.data.get("task")
template_data = get_task_template_data(
project_doc, asset_doc, task_name)
template_data.update(copy.deepcopy(asset_data))
instance.data["anatomyData"].update(template_data)
instance.data["assetEntity"] = asset_doc
def _create_hierarchy(self, context, project_name):
hierarchy_context = self._filter_hierarchy(context)
if not hierarchy_context:
self.log.debug("All folders were filtered out")

View file

@ -1,105 +0,0 @@
"""This module is used for command line exporting of ASS files.
WARNING:
This need to be rewriten to be able use it in Pype 3!
"""
import os
import argparse
import logging
import subprocess
import platform
try:
from shutil import which
except ImportError:
# we are in python < 3.3
def which(command):
path = os.getenv('PATH')
for p in path.split(os.path.pathsep):
p = os.path.join(p, command)
if os.path.exists(p) and os.access(p, os.X_OK):
return p
handler = logging.basicConfig()
log = logging.getLogger("Publish Image Sequences")
log.setLevel(logging.DEBUG)
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
def __main__():
parser = argparse.ArgumentParser()
parser.add_argument("--paths",
nargs="*",
default=[],
help="The filepaths to publish. This can be a "
"directory or a path to a .json publish "
"configuration.")
parser.add_argument("--gui",
default=False,
action="store_true",
help="Whether to run Pyblish in GUI mode.")
parser.add_argument("--pype", help="Pype root")
kwargs, args = parser.parse_known_args()
print("Running pype ...")
auto_pype_root = os.path.dirname(os.path.abspath(__file__))
auto_pype_root = os.path.abspath(auto_pype_root + "../../../../..")
auto_pype_root = os.environ.get('OPENPYPE_SETUP_PATH') or auto_pype_root
if os.environ.get('OPENPYPE_SETUP_PATH'):
print("Got Pype location from environment: {}".format(
os.environ.get('OPENPYPE_SETUP_PATH')))
pype_command = "openpype.ps1"
if platform.system().lower() == "linux":
pype_command = "pype"
elif platform.system().lower() == "windows":
pype_command = "openpype.bat"
if kwargs.pype:
pype_root = kwargs.pype
else:
# test if pype.bat / pype is in the PATH
# if it is, which() will return its path and we use that.
# if not, we use auto_pype_root path. Caveat of that one is
# that it can be UNC path and that will not work on windows.
pype_path = which(pype_command)
if pype_path:
pype_root = os.path.dirname(pype_path)
else:
pype_root = auto_pype_root
print("Set pype root to: {}".format(pype_root))
print("Paths: {}".format(kwargs.paths or [os.getcwd()]))
# paths = kwargs.paths or [os.environ.get("OPENPYPE_METADATA_FILE")] or [os.getcwd()] # noqa
mayabatch = os.environ.get("AVALON_APP_NAME").replace("maya", "mayabatch")
args = [
os.path.join(pype_root, pype_command),
"launch",
"--app",
mayabatch,
"-script",
os.path.join(pype_root, "repos", "pype",
"pype", "scripts", "export_maya_ass_sequence.mel")
]
print("Pype command: {}".format(" ".join(args)))
# Forcing forwaring the environment because environment inheritance does
# not always work.
# Cast all values in environment to str to be safe
env = {k: str(v) for k, v in os.environ.items()}
exit_code = subprocess.call(args, env=env)
if exit_code != 0:
raise RuntimeError("Publishing failed.")
if __name__ == '__main__':
__main__()

View file

@ -1,67 +0,0 @@
/*
Script to export specified layer as ass files.
Attributes:
scene_file (str): Name of the scene to load.
start (int): Start frame.
end (int): End frame.
step (int): Step size.
output_path (str): File output path.
render_layer (str): Name of render layer.
*/
$scene_file=`getenv "OPENPYPE_ASS_EXPORT_SCENE_FILE"`;
$step=`getenv "OPENPYPE_ASS_EXPORT_STEP"`;
$start=`getenv "OPENPYPE_ASS_EXPORT_START"`;
$end=`getenv "OPENPYPE_ASS_EXPORT_END"`;
$file_path=`getenv "OPENPYPE_ASS_EXPORT_OUTPUT"`;
$render_layer = `getenv "OPENPYPE_ASS_EXPORT_RENDER_LAYER"`;
print("*** ASS Export Plugin\n");
if ($scene_file == "") {
print("!!! cannot determine scene file\n");
quit -a -ex -1;
}
if ($step == "") {
print("!!! cannot determine step size\n");
quit -a -ex -1;
}
if ($start == "") {
print("!!! cannot determine start frame\n");
quit -a -ex -1;
}
if ($end == "") {
print("!!! cannot determine end frame\n");
quit -a -ex -1;
}
if ($file_path == "") {
print("!!! cannot determine output file\n");
quit -a -ex -1;
}
if ($render_layer == "") {
print("!!! cannot determine render layer\n");
quit -a -ex -1;
}
print(">>> Opening Scene [ " + $scene_file + " ]\n");
// open scene
file -o -f $scene_file;
// switch to render layer
print(">>> Switching layer [ "+ $render_layer + " ]\n");
editRenderLayerGlobals -currentRenderLayer $render_layer;
// export
print(">>> Exporting to [ " + $file_path + " ]\n");
arnoldExportAss -mask 255 -sl 1 -ll 1 -bb 1 -sf $start -se $end -b -fs $step;
print("--- Done\n");

View file

@ -1,241 +0,0 @@
import os
import re
import sys
import logging
from openpype.client import get_asset_by_name, get_versions
# Pipeline imports
from openpype.hosts.fusion import api
import openpype.hosts.fusion.api.lib as fusion_lib
# Config imports
from openpype.lib import version_up
from openpype.pipeline import (
install_host,
registered_host,
legacy_io,
get_current_project_name,
)
from openpype.pipeline.context_tools import get_workdir_from_session
from openpype.pipeline.version_start import get_versioning_start
log = logging.getLogger("Update Slap Comp")
def _format_version_folder(folder):
"""Format a version folder based on the filepath
Args:
folder: file path to a folder
Returns:
str: new version folder name
"""
new_version = get_versioning_start(
get_current_project_name(),
"fusion",
family="workfile"
)
if os.path.isdir(folder):
re_version = re.compile(r"v\d+$")
versions = [i for i in os.listdir(folder) if os.path.isdir(i)
and re_version.match(i)]
if versions:
# ensure the "v" is not included
new_version = int(max(versions)[1:]) + 1
version_folder = "v{:03d}".format(new_version)
return version_folder
def _get_fusion_instance():
fusion = getattr(sys.modules["__main__"], "fusion", None)
if fusion is None:
try:
# Support for FuScript.exe, BlackmagicFusion module for py2 only
import BlackmagicFusion as bmf
fusion = bmf.scriptapp("Fusion")
except ImportError:
raise RuntimeError("Could not find a Fusion instance")
return fusion
def _format_filepath(session):
project = session["AVALON_PROJECT"]
asset = session["AVALON_ASSET"]
# Save updated slap comp
work_path = get_workdir_from_session(session)
walk_to_dir = os.path.join(work_path, "scenes", "slapcomp")
slapcomp_dir = os.path.abspath(walk_to_dir)
# Ensure destination exists
if not os.path.isdir(slapcomp_dir):
log.warning("Folder did not exist, creating folder structure")
os.makedirs(slapcomp_dir)
# Compute output path
new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset)
new_filepath = os.path.join(slapcomp_dir, new_filename)
# Create new unqiue filepath
if os.path.exists(new_filepath):
new_filepath = version_up(new_filepath)
return new_filepath
def _update_savers(comp, session):
"""Update all savers of the current comp to ensure the output is correct
Args:
comp (object): current comp instance
session (dict): the current Avalon session
Returns:
None
"""
new_work = get_workdir_from_session(session)
renders = os.path.join(new_work, "renders")
version_folder = _format_version_folder(renders)
renders_version = os.path.join(renders, version_folder)
comp.Print("New renders to: %s\n" % renders)
with api.comp_lock_and_undo_chunk(comp):
savers = comp.GetToolList(False, "Saver").values()
for saver in savers:
filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0]
filename = os.path.basename(filepath)
new_path = os.path.join(renders_version, filename)
saver["Clip"] = new_path
def update_frame_range(comp, representations):
"""Update the frame range of the comp and render length
The start and end frame are based on the lowest start frame and the highest
end frame
Args:
comp (object): current focused comp
representations (list) collection of dicts
Returns:
None
"""
version_ids = [r["parent"] for r in representations]
project_name = get_current_project_name()
versions = list(get_versions(project_name, version_ids=version_ids))
start = min(v["data"]["frameStart"] for v in versions)
end = max(v["data"]["frameEnd"] for v in versions)
fusion_lib.update_frame_range(start, end, comp=comp)
def switch(asset_name, filepath=None, new=True):
"""Switch the current containers of the file to the other asset (shot)
Args:
filepath (str): file path of the comp file
asset_name (str): name of the asset (shot)
new (bool): Save updated comp under a different name
Returns:
comp path (str): new filepath of the updated comp
"""
# If filepath provided, ensure it is valid absolute path
if filepath is not None:
if not os.path.isabs(filepath):
filepath = os.path.abspath(filepath)
assert os.path.exists(filepath), "%s must exist " % filepath
# Assert asset name exists
# It is better to do this here then to wait till switch_shot does it
project_name = get_current_project_name()
asset = get_asset_by_name(project_name, asset_name)
assert asset, "Could not find '%s' in the database" % asset_name
# Go to comp
if not filepath:
current_comp = api.get_current_comp()
assert current_comp is not None, "Could not find current comp"
else:
fusion = _get_fusion_instance()
current_comp = fusion.LoadComp(filepath, quiet=True)
assert current_comp is not None, "Fusion could not load '%s'" % filepath
host = registered_host()
containers = list(host.ls())
assert containers, "Nothing to update"
representations = []
for container in containers:
try:
representation = fusion_lib.switch_item(container,
asset_name=asset_name)
representations.append(representation)
except Exception as e:
current_comp.Print("Error in switching! %s\n" % e.message)
message = "Switched %i Loaders of the %i\n" % (len(representations),
len(containers))
current_comp.Print(message)
# Build the session to switch to
switch_to_session = legacy_io.Session.copy()
switch_to_session["AVALON_ASSET"] = asset['name']
if new:
comp_path = _format_filepath(switch_to_session)
# Update savers output based on new session
_update_savers(current_comp, switch_to_session)
else:
comp_path = version_up(filepath)
current_comp.Print(comp_path)
current_comp.Print("\nUpdating frame range")
update_frame_range(current_comp, representations)
current_comp.Save(comp_path)
return comp_path
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Switch to a shot within an"
"existing comp file")
parser.add_argument("--file_path",
type=str,
default=True,
help="File path of the comp to use")
parser.add_argument("--asset_name",
type=str,
default=True,
help="Name of the asset (shot) to switch")
args, unknown = parser.parse_args()
install_host(api)
switch(args.asset_name, args.file_path)
sys.exit(0)

View file

@ -616,6 +616,23 @@ def _convert_maya_project_settings(ayon_settings, output):
output["maya"] = ayon_maya
def _convert_3dsmax_project_settings(ayon_settings, output):
if "max" not in ayon_settings:
return
ayon_max = ayon_settings["max"]
_convert_host_imageio(ayon_max)
if "PointCloud" in ayon_max:
point_cloud_attribute = ayon_max["PointCloud"]["attribute"]
new_point_cloud_attribute = {
item["name"]: item["value"]
for item in point_cloud_attribute
}
ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute
output["max"] = ayon_max
def _convert_nuke_knobs(knobs):
new_knobs = []
for knob in knobs:
@ -737,6 +754,17 @@ def _convert_nuke_project_settings(ayon_settings, output):
item_filter["subsets"] = item_filter.pop("product_names")
item_filter["families"] = item_filter.pop("product_types")
reformat_nodes_config = item.get("reformat_nodes_config") or {}
reposition_nodes = reformat_nodes_config.get(
"reposition_nodes") or []
for reposition_node in reposition_nodes:
if "knobs" not in reposition_node:
continue
reposition_node["knobs"] = _convert_nuke_knobs(
reposition_node["knobs"]
)
name = item.pop("name")
new_review_data_outputs[name] = item
ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs
@ -1261,6 +1289,7 @@ def convert_project_settings(ayon_settings, default_settings):
_convert_flame_project_settings(ayon_settings, output)
_convert_fusion_project_settings(ayon_settings, output)
_convert_maya_project_settings(ayon_settings, output)
_convert_3dsmax_project_settings(ayon_settings, output)
_convert_nuke_project_settings(ayon_settings, output)
_convert_hiero_project_settings(ayon_settings, output)
_convert_photoshop_project_settings(ayon_settings, output)

View file

@ -19,6 +19,7 @@ from openpype.tools.utils import (
CustomTextComboBox,
FocusSpinBox,
FocusDoubleSpinBox,
MultiSelectionComboBox,
)
from openpype.widgets.nice_checkbox import NiceCheckbox
@ -412,10 +413,19 @@ class EnumAttrWidget(_BaseAttrDefWidget):
self._multivalue = False
super(EnumAttrWidget, self).__init__(*args, **kwargs)
@property
def multiselection(self):
return self.attr_def.multiselection
def _ui_init(self):
input_widget = CustomTextComboBox(self)
combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)
input_widget.setItemDelegate(combo_delegate)
if self.multiselection:
input_widget = MultiSelectionComboBox(self)
else:
input_widget = CustomTextComboBox(self)
combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)
input_widget.setItemDelegate(combo_delegate)
self._combo_delegate = combo_delegate
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
@ -427,9 +437,11 @@ class EnumAttrWidget(_BaseAttrDefWidget):
if idx >= 0:
input_widget.setCurrentIndex(idx)
input_widget.currentIndexChanged.connect(self._on_value_change)
if self.multiselection:
input_widget.value_changed.connect(self._on_value_change)
else:
input_widget.currentIndexChanged.connect(self._on_value_change)
self._combo_delegate = combo_delegate
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
@ -442,17 +454,40 @@ class EnumAttrWidget(_BaseAttrDefWidget):
self.value_changed.emit(new_value, self.attr_def.id)
def current_value(self):
if self.multiselection:
return self._input_widget.value()
idx = self._input_widget.currentIndex()
return self._input_widget.itemData(idx)
def _multiselection_multivalue_prep(self, values):
final = None
multivalue = False
for value in values:
value = set(value)
if final is None:
final = value
elif multivalue or final != value:
final |= value
multivalue = True
return list(final), multivalue
def set_value(self, value, multivalue=False):
if multivalue:
set_value = set(value)
if len(set_value) == 1:
multivalue = False
value = tuple(set_value)[0]
if self.multiselection:
value, multivalue = self._multiselection_multivalue_prep(
value)
else:
set_value = set(value)
if len(set_value) == 1:
multivalue = False
value = tuple(set_value)[0]
if not multivalue:
if self.multiselection:
self._input_widget.blockSignals(True)
self._input_widget.set_value(value)
self._input_widget.blockSignals(False)
elif not multivalue:
idx = self._input_widget.findData(value)
cur_idx = self._input_widget.currentIndex()
if idx != cur_idx and idx >= 0:

View file

@ -4,6 +4,7 @@ from qtpy import QtWidgets, QtCore, QtGui
from openpype.widgets.sliders import NiceSlider
from openpype.tools.settings import CHILD_OFFSET
from openpype.tools.utils import MultiSelectionComboBox
from openpype.settings.entities.exceptions import BaseInvalidValue
from .widgets import (
@ -15,7 +16,6 @@ from .widgets import (
SettingsNiceCheckbox,
SettingsLineEdit
)
from .multiselection_combobox import MultiSelectionComboBox
from .wrapper_widgets import (
WrapperWidget,
CollapsibleWrapper,

View file

@ -38,6 +38,7 @@ from .models import (
from .overlay_messages import (
MessageOverlayObject,
)
from .multiselection_combobox import MultiSelectionComboBox
__all__ = (
@ -78,4 +79,6 @@ __all__ = (
"RecursiveSortFilterProxyModel",
"MessageOverlayObject",
"MultiSelectionComboBox",
)

View file

@ -170,8 +170,12 @@ def get_openpype_qt_app():
if attr is not None:
QtWidgets.QApplication.setAttribute(attr)
if hasattr(
QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"
policy = os.getenv("QT_SCALE_FACTOR_ROUNDING_POLICY")
if (
hasattr(
QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"
)
and not policy
):
QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(
QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough

View file

@ -1,9 +1,10 @@
from qtpy import QtCore, QtGui, QtWidgets
from openpype.tools.utils.lib import (
from .lib import (
checkstate_int_to_enum,
checkstate_enum_to_int,
)
from openpype.tools.utils.constants import (
from .constants import (
CHECKED_INT,
UNCHECKED_INT,
ITEM_IS_USER_TRISTATE,
@ -60,12 +61,25 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)
self._initial_mouse_pos = None
self._separator = separator
self.placeholder_text = placeholder
self.delegate = ComboItemDelegate(self)
self.setItemDelegate(self.delegate)
self._placeholder_text = placeholder
delegate = ComboItemDelegate(self)
self.setItemDelegate(delegate)
self.lines = {}
self.item_height = None
self._lines = {}
self._item_height = None
self._custom_text = None
self._delegate = delegate
def get_placeholder_text(self):
return self._placeholder_text
def set_placeholder_text(self, text):
self._placeholder_text = text
self._update_size_hint()
def set_custom_text(self, text):
self._custom_text = text
self._update_size_hint()
def focusInEvent(self, event):
self.focused_in.emit()
@ -158,7 +172,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
if new_state is not None:
model.setData(current_index, new_state, QtCore.Qt.CheckStateRole)
self.view().update(current_index)
self.update_size_hint()
self._update_size_hint()
self.value_changed.emit()
return True
@ -182,25 +196,33 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
self.initStyleOption(option)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)
# draw the icon and text
items = self.checked_items_text()
if not items:
option.currentText = self.placeholder_text
# draw the icon and text
draw_text = True
combotext = None
if self._custom_text is not None:
combotext = self._custom_text
elif not items:
combotext = self._placeholder_text
else:
draw_text = False
if draw_text:
option.currentText = combotext
option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
return
font_metricts = self.fontMetrics()
if self.item_height is None:
if self._item_height is None:
self.updateGeometry()
self.update()
return
for line, items in self.lines.items():
for line, items in self._lines.items():
top_y = (
option.rect.top()
+ (line * self.item_height)
+ (line * self._item_height)
+ self.top_bottom_margins
)
left_x = option.rect.left() + self.left_offset
@ -210,7 +232,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
label_rect.moveTop(top_y)
label_rect.moveLeft(left_x)
label_rect.setHeight(self.item_height)
label_rect.setHeight(self._item_height)
label_rect.setWidth(
label_rect.width() + self.left_right_padding
)
@ -239,14 +261,18 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
def resizeEvent(self, *args, **kwargs):
super(MultiSelectionComboBox, self).resizeEvent(*args, **kwargs)
self.update_size_hint()
self._update_size_hint()
def update_size_hint(self):
self.lines = {}
def _update_size_hint(self):
if self._custom_text is not None:
self.update()
return
self._lines = {}
items = self.checked_items_text()
if not items:
self.update()
self.repaint()
return
option = QtWidgets.QStyleOptionComboBox()
@ -259,7 +285,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
total_width = option.rect.width() - btn_rect.width()
line = 0
self.lines = {line: []}
self._lines = {line: []}
font_metricts = self.fontMetrics()
default_left_x = 0 + self.left_offset
@ -270,18 +296,18 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
right_x = left_x + width
if right_x > total_width:
left_x = int(default_left_x)
if self.lines.get(line):
if self._lines.get(line):
line += 1
self.lines[line] = [item]
self._lines[line] = [item]
left_x += width
else:
self.lines[line] = [item]
self._lines[line] = [item]
line += 1
else:
if line in self.lines:
self.lines[line].append(item)
if line in self._lines:
self._lines[line].append(item)
else:
self.lines[line] = [item]
self._lines[line] = [item]
left_x = left_x + width + self.item_spacing
self.update()
@ -289,18 +315,20 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
def sizeHint(self):
value = super(MultiSelectionComboBox, self).sizeHint()
lines = len(self.lines)
if lines == 0:
lines = 1
lines = 1
if self._custom_text is None:
lines = len(self._lines)
if lines == 0:
lines = 1
if self.item_height is None:
self.item_height = (
if self._item_height is None:
self._item_height = (
self.fontMetrics().height()
+ (2 * self.top_bottom_padding)
+ (2 * self.top_bottom_margins)
)
value.setHeight(
(lines * self.item_height)
(lines * self._item_height)
+ (2 * self.top_bottom_margins)
)
return value
@ -316,7 +344,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
else:
check_state = UNCHECKED_INT
self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole)
self.update_size_hint()
self._update_size_hint()
def value(self):
items = list()

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.16.5-nightly.3"
__version__ = "3.16.5-nightly.4"

View file

@ -5,7 +5,7 @@ from ayon_server.settings import BaseSettingsModel
class CreateRenderPlugin(BaseSettingsModel):
mark_for_review: bool = Field(True, title="Review")
defaults: list[str] = Field(
default_variants: list[str] = Field(
default_factory=list,
title="Default Variants"
)

View file

@ -40,7 +40,7 @@ DEFAULT_AFTEREFFECTS_SETTING = {
"create": {
"RenderCreator": {
"mark_for_review": True,
"defaults": [
"default_variants": [
"Main"
]
}

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.1"
__version__ = "0.1.2"

View file

@ -4,6 +4,7 @@ from ayon_server.settings import (
BaseSettingsModel,
MultiplatformPathListModel,
ensure_unique_names,
task_types_enum,
)
from ayon_server.exceptions import BadRequestException
@ -38,13 +39,52 @@ class CoreImageIOConfigModel(BaseSettingsModel):
class CoreImageIOBaseModel(BaseSettingsModel):
activate_global_color_management: bool = Field(
False,
title="Override global OCIO config"
title="Enable Color Management"
)
ocio_config: CoreImageIOConfigModel = Field(
default_factory=CoreImageIOConfigModel, title="OCIO config"
default_factory=CoreImageIOConfigModel,
title="OCIO config"
)
file_rules: CoreImageIOFileRulesModel = Field(
default_factory=CoreImageIOFileRulesModel, title="File Rules"
default_factory=CoreImageIOFileRulesModel,
title="File Rules"
)
class VersionStartCategoryProfileModel(BaseSettingsModel):
_layout = "expanded"
host_names: list[str] = Field(
default_factory=list,
title="Host names"
)
task_types: list[str] = Field(
default_factory=list,
title="Task types",
enum_resolver=task_types_enum
)
task_names: list[str] = Field(
default_factory=list,
title="Task names"
)
product_types: list[str] = Field(
default_factory=list,
title="Product types"
)
product_names: list[str] = Field(
default_factory=list,
title="Product names"
)
version_start: int = Field(
1,
title="Version Start",
ge=0
)
class VersionStartCategoryModel(BaseSettingsModel):
profiles: list[VersionStartCategoryProfileModel] = Field(
default_factory=list,
title="Profiles"
)
@ -61,6 +101,10 @@ class CoreSettings(BaseSettingsModel):
default_factory=GlobalToolsModel,
title="Tools"
)
version_start_category: VersionStartCategoryModel = Field(
default_factory=VersionStartCategoryModel,
title="Version start"
)
imageio: CoreImageIOBaseModel = Field(
default_factory=CoreImageIOBaseModel,
title="Color Management (ImageIO)"
@ -131,6 +175,9 @@ DEFAULT_VALUES = {
"studio_code": "",
"environments": "{}",
"tools": DEFAULT_TOOLS_VALUES,
"version_start_category": {
"profiles": []
},
"publish": DEFAULT_PUBLISH_VALUES,
"project_folder_structure": json.dumps({
"__project_root__": {

View file

@ -1 +1 @@
__version__ = "0.1.1"
__version__ = "0.1.2"

View file

@ -44,6 +44,6 @@ class RenderSettingsModel(BaseSettingsModel):
DEFAULT_RENDER_SETTINGS = {
"default_render_image_folder": "renders/3dsmax",
"aov_separator": "underscore",
"image_format": "png",
"image_format": "exr",
"multipass": True
}

View file

@ -252,7 +252,9 @@ DEFAULT_CREATORS_SETTINGS = {
},
"CreateUnrealSkeletalMesh": {
"enabled": True,
"default_variants": [],
"default_variants": [
"Main",
],
"joint_hints": "jnt_org"
},
"CreateMultiverseLook": {

View file

@ -288,5 +288,22 @@ DEFAULT_SIMPLE_CREATORS = [
"allow_multiple_items": True,
"allow_version_control": False,
"extensions": []
},
{
"product_type": "audio",
"identifier": "",
"label": "Audio ",
"icon": "fa5s.file-audio",
"default_variants": [
"Main"
],
"description": "Audio product",
"detailed_description": "Audio files for review or final delivery",
"allow_sequences": False,
"allow_multiple_items": False,
"allow_version_control": False,
"extensions": [
".wav"
]
}
]