mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into enhancement/OP-2956_move-host-install
This commit is contained in:
commit
2d9b13c2dd
56 changed files with 1204 additions and 753 deletions
|
|
@ -29,12 +29,12 @@ def add_implementation_envs(env, _app):
|
|||
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
|
||||
)
|
||||
for path in openpype_blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
if path:
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
|
||||
for path in blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
if path:
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
# Remove implementation path from user script paths as is set to
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
|
||||
for path in old_hiero_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def add_implementation_envs(env, _app):
|
|||
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
|
||||
|
||||
for path in old_houdini_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
@ -23,7 +23,7 @@ def add_implementation_envs(env, _app):
|
|||
new_houdini_path.append(norm_path)
|
||||
|
||||
for path in old_houdini_menu_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_python_path = env.get("PYTHONPATH") or ""
|
||||
for path in old_python_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
202
openpype/hosts/maya/api/fbx.py
Normal file
202
openpype/hosts/maya/api/fbx.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Tools to work with FBX."""
|
||||
import logging
|
||||
|
||||
from pyblish.api import Instance
|
||||
|
||||
from maya import cmds # noqa
|
||||
import maya.mel as mel # noqa
|
||||
|
||||
|
||||
class FBXExtractor:
|
||||
"""Extract FBX from Maya.
|
||||
|
||||
This extracts reproducible FBX exports ignoring any of the settings set
|
||||
on the local machine in the FBX export options window.
|
||||
|
||||
All export settings are applied with the `FBXExport*` commands prior
|
||||
to the `FBXExport` call itself. The options can be overridden with
|
||||
their
|
||||
nice names as seen in the "options" property on this class.
|
||||
|
||||
For more information on FBX exports see:
|
||||
- https://knowledge.autodesk.com/support/maya/learn-explore/caas
|
||||
/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
|
||||
-9CB19C28F4E0-htm.html
|
||||
- http://forums.cgsociety.org/archive/index.php?t-1032853.html
|
||||
- https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
|
||||
/LKs9hakE28kJ
|
||||
|
||||
"""
|
||||
@property
|
||||
def options(self):
|
||||
"""Overridable options for FBX Export
|
||||
|
||||
Given in the following format
|
||||
- {NAME: EXPECTED TYPE}
|
||||
|
||||
If the overridden option's type does not match,
|
||||
the option is not included and a warning is logged.
|
||||
|
||||
"""
|
||||
|
||||
return {
|
||||
"cameras": bool,
|
||||
"smoothingGroups": bool,
|
||||
"hardEdges": bool,
|
||||
"tangents": bool,
|
||||
"smoothMesh": bool,
|
||||
"instances": bool,
|
||||
# "referencedContainersContent": bool, # deprecated in Maya 2016+
|
||||
"bakeComplexAnimation": int,
|
||||
"bakeComplexStart": int,
|
||||
"bakeComplexEnd": int,
|
||||
"bakeComplexStep": int,
|
||||
"bakeResampleAnimation": bool,
|
||||
"animationOnly": bool,
|
||||
"useSceneName": bool,
|
||||
"quaternion": str, # "euler"
|
||||
"shapes": bool,
|
||||
"skins": bool,
|
||||
"constraints": bool,
|
||||
"lights": bool,
|
||||
"embeddedTextures": bool,
|
||||
"inputConnections": bool,
|
||||
"upAxis": str, # x, y or z,
|
||||
"triangulate": bool
|
||||
}
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""The default options for FBX extraction.
|
||||
|
||||
This includes shapes, skins, constraints, lights and incoming
|
||||
connections and exports with the Y-axis as up-axis.
|
||||
|
||||
By default this uses the time sliders start and end time.
|
||||
|
||||
"""
|
||||
|
||||
start_frame = int(cmds.playbackOptions(query=True,
|
||||
animationStartTime=True))
|
||||
end_frame = int(cmds.playbackOptions(query=True,
|
||||
animationEndTime=True))
|
||||
|
||||
return {
|
||||
"cameras": False,
|
||||
"smoothingGroups": True,
|
||||
"hardEdges": False,
|
||||
"tangents": False,
|
||||
"smoothMesh": True,
|
||||
"instances": False,
|
||||
"bakeComplexAnimation": True,
|
||||
"bakeComplexStart": start_frame,
|
||||
"bakeComplexEnd": end_frame,
|
||||
"bakeComplexStep": 1,
|
||||
"bakeResampleAnimation": True,
|
||||
"animationOnly": False,
|
||||
"useSceneName": False,
|
||||
"quaternion": "euler",
|
||||
"shapes": True,
|
||||
"skins": True,
|
||||
"constraints": False,
|
||||
"lights": True,
|
||||
"embeddedTextures": False,
|
||||
"inputConnections": True,
|
||||
"upAxis": "y",
|
||||
"triangulate": False
|
||||
}
|
||||
|
||||
def __init__(self, log=None):
|
||||
# Ensure FBX plug-in is loaded
|
||||
self.log = log or logging.getLogger(self.__class__.__name__)
|
||||
cmds.loadPlugin("fbxmaya", quiet=True)
|
||||
|
||||
def parse_overrides(self, instance, options):
|
||||
"""Inspect data of instance to determine overridden options
|
||||
|
||||
An instance may supply any of the overridable options
|
||||
as data, the option is then added to the extraction.
|
||||
|
||||
"""
|
||||
|
||||
for key in instance.data:
|
||||
if key not in self.options:
|
||||
continue
|
||||
|
||||
# Ensure the data is of correct type
|
||||
value = instance.data[key]
|
||||
if not isinstance(value, self.options[key]):
|
||||
self.log.warning(
|
||||
"Overridden attribute {key} was of "
|
||||
"the wrong type: {invalid_type} "
|
||||
"- should have been {valid_type}".format(
|
||||
key=key,
|
||||
invalid_type=type(value).__name__,
|
||||
valid_type=self.options[key].__name__))
|
||||
continue
|
||||
|
||||
options[key] = value
|
||||
|
||||
return options
|
||||
|
||||
def set_options_from_instance(self, instance):
|
||||
# type: (Instance) -> None
|
||||
"""Sets FBX export options from data in the instance.
|
||||
|
||||
Args:
|
||||
instance (Instance): Instance data.
|
||||
|
||||
"""
|
||||
# Parse export options
|
||||
options = self.default_options
|
||||
options = self.parse_overrides(instance, options)
|
||||
self.log.info("Export options: {0}".format(options))
|
||||
|
||||
# Collect the start and end including handles
|
||||
start = instance.data.get("frameStartHandle") or \
|
||||
instance.context.data.get("frameStartHandle")
|
||||
end = instance.data.get("frameEndHandle") or \
|
||||
instance.context.data.get("frameEndHandle")
|
||||
|
||||
options['bakeComplexStart'] = start
|
||||
options['bakeComplexEnd'] = end
|
||||
|
||||
# First apply the default export settings to be fully consistent
|
||||
# each time for successive publishes
|
||||
mel.eval("FBXResetExport")
|
||||
|
||||
# Apply the FBX overrides through MEL since the commands
|
||||
# only work correctly in MEL according to online
|
||||
# available discussions on the topic
|
||||
_iteritems = getattr(options, "iteritems", options.items)
|
||||
for option, value in _iteritems():
|
||||
key = option[0].upper() + option[1:] # uppercase first letter
|
||||
|
||||
# Boolean must be passed as lower-case strings
|
||||
# as to MEL standards
|
||||
if isinstance(value, bool):
|
||||
value = str(value).lower()
|
||||
|
||||
template = "FBXExport{0} {1}" if key == "UpAxis" else \
|
||||
"FBXExport{0} -v {1}" # noqa
|
||||
cmd = template.format(key, value)
|
||||
self.log.info(cmd)
|
||||
mel.eval(cmd)
|
||||
|
||||
# Never show the UI or generate a log
|
||||
mel.eval("FBXExportShowUI -v false")
|
||||
mel.eval("FBXExportGenerateLog -v false")
|
||||
|
||||
@staticmethod
|
||||
def export(members, path):
|
||||
# type: (list, str) -> None
|
||||
"""Export members as FBX with given path.
|
||||
|
||||
Args:
|
||||
members (list): List of members to export.
|
||||
path (str): Path to use for export.
|
||||
|
||||
"""
|
||||
cmds.select(members, r=True, noExpand=True)
|
||||
mel.eval('FBXExport -f "{}" -s'.format(path))
|
||||
|
|
@ -3139,11 +3139,20 @@ def set_colorspace():
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def root_parent(nodes):
|
||||
# type: (list) -> list
|
||||
def parent_nodes(nodes, parent=None):
|
||||
# type: (list, str) -> list
|
||||
"""Context manager to un-parent provided nodes and return them back."""
|
||||
import pymel.core as pm # noqa
|
||||
|
||||
parent_node = None
|
||||
delete_parent = False
|
||||
|
||||
if parent:
|
||||
if not cmds.objExists(parent):
|
||||
parent_node = pm.createNode("transform", n=parent, ss=False)
|
||||
delete_parent = True
|
||||
else:
|
||||
parent_node = pm.PyNode(parent)
|
||||
node_parents = []
|
||||
for node in nodes:
|
||||
n = pm.PyNode(node)
|
||||
|
|
@ -3154,9 +3163,14 @@ def root_parent(nodes):
|
|||
node_parents.append((n, root))
|
||||
try:
|
||||
for node in node_parents:
|
||||
node[0].setParent(world=True)
|
||||
if not parent:
|
||||
node[0].setParent(world=True)
|
||||
else:
|
||||
node[0].setParent(parent_node)
|
||||
yield
|
||||
finally:
|
||||
for node in node_parents:
|
||||
if node[1]:
|
||||
node[0].setParent(node[1])
|
||||
if delete_parent:
|
||||
pm.delete(parent_node)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator for Unreal Skeletal Meshes."""
|
||||
from openpype.hosts.maya.api import plugin, lib
|
||||
from avalon.api import Session
|
||||
from maya import cmds # noqa
|
||||
|
||||
|
||||
class CreateUnrealSkeletalMesh(plugin.Creator):
|
||||
"""Unreal Static Meshes with collisions."""
|
||||
name = "staticMeshMain"
|
||||
label = "Unreal - Skeletal Mesh"
|
||||
family = "skeletalMesh"
|
||||
icon = "thumbs-up"
|
||||
dynamic_subset_keys = ["asset"]
|
||||
|
||||
joint_hints = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_dynamic_data(
|
||||
cls, variant, task_name, asset_id, project_name, host_name
|
||||
):
|
||||
dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data(
|
||||
variant, task_name, asset_id, project_name, host_name
|
||||
)
|
||||
dynamic_data["asset"] = Session.get("AVALON_ASSET")
|
||||
return dynamic_data
|
||||
|
||||
def process(self):
|
||||
self.name = "{}_{}".format(self.family, self.name)
|
||||
with lib.undo_chunk():
|
||||
instance = super(CreateUnrealSkeletalMesh, self).process()
|
||||
content = cmds.sets(instance, query=True)
|
||||
|
||||
# empty set and process its former content
|
||||
cmds.sets(content, rm=instance)
|
||||
geometry_set = cmds.sets(name="geometry_SET", empty=True)
|
||||
joints_set = cmds.sets(name="joints_SET", empty=True)
|
||||
|
||||
cmds.sets([geometry_set, joints_set], forceElement=instance)
|
||||
members = cmds.ls(content) or []
|
||||
|
||||
for node in members:
|
||||
if node in self.joint_hints:
|
||||
cmds.sets(node, forceElement=joints_set)
|
||||
else:
|
||||
cmds.sets(node, forceElement=geometry_set)
|
||||
|
|
@ -10,7 +10,7 @@ class CreateUnrealStaticMesh(plugin.Creator):
|
|||
"""Unreal Static Meshes with collisions."""
|
||||
name = "staticMeshMain"
|
||||
label = "Unreal - Static Mesh"
|
||||
family = "unrealStaticMesh"
|
||||
family = "staticMesh"
|
||||
icon = "cube"
|
||||
dynamic_subset_keys = ["asset"]
|
||||
|
||||
|
|
@ -28,10 +28,10 @@ class CreateUnrealStaticMesh(plugin.Creator):
|
|||
variant, task_name, asset_id, project_name, host_name
|
||||
)
|
||||
dynamic_data["asset"] = Session.get("AVALON_ASSET")
|
||||
|
||||
return dynamic_data
|
||||
|
||||
def process(self):
|
||||
self.name = "{}_{}".format(self.family, self.name)
|
||||
with lib.undo_chunk():
|
||||
instance = super(CreateUnrealStaticMesh, self).process()
|
||||
content = cmds.sets(instance, query=True)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
"camera",
|
||||
"rig",
|
||||
"camerarig",
|
||||
"xgen"]
|
||||
"xgen",
|
||||
"staticMesh"]
|
||||
representations = ["ma", "abc", "fbx", "mb"]
|
||||
|
||||
label = "Reference"
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Cleanup leftover nodes."""
|
||||
from maya import cmds # noqa
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CleanNodesUp(pyblish.api.InstancePlugin):
|
||||
"""Cleans up the staging directory after a successful publish.
|
||||
|
||||
This will also clean published renders and delete their parent directories.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 10
|
||||
label = "Clean Nodes"
|
||||
optional = True
|
||||
active = True
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("cleanNodes"):
|
||||
self.log.info("Nothing to clean.")
|
||||
return
|
||||
|
||||
nodes_to_clean = instance.data.pop("cleanNodes", [])
|
||||
self.log.info("Removing {} nodes".format(len(nodes_to_clean)))
|
||||
for node in nodes_to_clean:
|
||||
try:
|
||||
cmds.delete(node)
|
||||
except ValueError:
|
||||
# object might be already deleted, don't complain about it
|
||||
pass
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from maya import cmds # noqa
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin):
|
||||
"""Collect Unreal Skeletal Mesh."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
label = "Collect Unreal Skeletal Meshes"
|
||||
families = ["skeletalMesh"]
|
||||
|
||||
def process(self, instance):
|
||||
frame = cmds.currentTime(query=True)
|
||||
instance.data["frameStart"] = frame
|
||||
instance.data["frameEnd"] = frame
|
||||
|
||||
geo_sets = [
|
||||
i for i in instance[:]
|
||||
if i.lower().startswith("geometry_set")
|
||||
]
|
||||
|
||||
joint_sets = [
|
||||
i for i in instance[:]
|
||||
if i.lower().startswith("joints_set")
|
||||
]
|
||||
|
||||
instance.data["geometry"] = []
|
||||
instance.data["joints"] = []
|
||||
|
||||
for geo_set in geo_sets:
|
||||
geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True)
|
||||
if geo_content:
|
||||
instance.data["geometry"] += geo_content
|
||||
|
||||
for join_set in joint_sets:
|
||||
join_content = cmds.ls(cmds.sets(join_set, query=True), long=True)
|
||||
if join_content:
|
||||
instance.data["joints"] += join_content
|
||||
|
|
@ -1,38 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from maya import cmds
|
||||
from maya import cmds # noqa
|
||||
import pyblish.api
|
||||
from pprint import pformat
|
||||
|
||||
|
||||
class CollectUnrealStaticMesh(pyblish.api.InstancePlugin):
|
||||
"""Collect Unreal Static Mesh
|
||||
|
||||
Ensures always only a single frame is extracted (current frame). This
|
||||
also sets correct FBX options for later extraction.
|
||||
|
||||
"""
|
||||
"""Collect Unreal Static Mesh."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
label = "Collect Unreal Static Meshes"
|
||||
families = ["unrealStaticMesh"]
|
||||
families = ["staticMesh"]
|
||||
|
||||
def process(self, instance):
|
||||
# add fbx family to trigger fbx extractor
|
||||
instance.data["families"].append("fbx")
|
||||
# take the name from instance (without the `S_` prefix)
|
||||
instance.data["staticMeshCombinedName"] = instance.name[2:]
|
||||
|
||||
geometry_set = [i for i in instance if i == "geometry_SET"]
|
||||
instance.data["membersToCombine"] = cmds.sets(
|
||||
geometry_set = [
|
||||
i for i in instance
|
||||
if i.startswith("geometry_SET")
|
||||
]
|
||||
instance.data["geometryMembers"] = cmds.sets(
|
||||
geometry_set, query=True)
|
||||
|
||||
collision_set = [i for i in instance if i == "collisions_SET"]
|
||||
self.log.info("geometry: {}".format(
|
||||
pformat(instance.data.get("geometryMembers"))))
|
||||
|
||||
collision_set = [
|
||||
i for i in instance
|
||||
if i.startswith("collisions_SET")
|
||||
]
|
||||
instance.data["collisionMembers"] = cmds.sets(
|
||||
collision_set, query=True)
|
||||
|
||||
# set fbx overrides on instance
|
||||
instance.data["smoothingGroups"] = True
|
||||
instance.data["smoothMesh"] = True
|
||||
instance.data["triangulate"] = True
|
||||
self.log.info("collisions: {}".format(
|
||||
pformat(instance.data.get("collisionMembers"))))
|
||||
|
||||
frame = cmds.currentTime(query=True)
|
||||
instance.data["frameStart"] = frame
|
||||
|
|
|
|||
|
|
@ -5,152 +5,29 @@ from maya import cmds # noqa
|
|||
import maya.mel as mel # noqa
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
root_parent,
|
||||
maintained_selection
|
||||
)
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
class ExtractFBX(openpype.api.Extractor):
|
||||
"""Extract FBX from Maya.
|
||||
|
||||
This extracts reproducible FBX exports ignoring any of the settings set
|
||||
on the local machine in the FBX export options window.
|
||||
|
||||
All export settings are applied with the `FBXExport*` commands prior
|
||||
to the `FBXExport` call itself. The options can be overridden with their
|
||||
nice names as seen in the "options" property on this class.
|
||||
|
||||
For more information on FBX exports see:
|
||||
- https://knowledge.autodesk.com/support/maya/learn-explore/caas
|
||||
/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
|
||||
-9CB19C28F4E0-htm.html
|
||||
- http://forums.cgsociety.org/archive/index.php?t-1032853.html
|
||||
- https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
|
||||
/LKs9hakE28kJ
|
||||
This extracts reproducible FBX exports ignoring any of the
|
||||
settings set on the local machine in the FBX export options window.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract FBX"
|
||||
families = ["fbx"]
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Overridable options for FBX Export
|
||||
|
||||
Given in the following format
|
||||
- {NAME: EXPECTED TYPE}
|
||||
|
||||
If the overridden option's type does not match,
|
||||
the option is not included and a warning is logged.
|
||||
|
||||
"""
|
||||
|
||||
return {
|
||||
"cameras": bool,
|
||||
"smoothingGroups": bool,
|
||||
"hardEdges": bool,
|
||||
"tangents": bool,
|
||||
"smoothMesh": bool,
|
||||
"instances": bool,
|
||||
# "referencedContainersContent": bool, # deprecated in Maya 2016+
|
||||
"bakeComplexAnimation": int,
|
||||
"bakeComplexStart": int,
|
||||
"bakeComplexEnd": int,
|
||||
"bakeComplexStep": int,
|
||||
"bakeResampleAnimation": bool,
|
||||
"animationOnly": bool,
|
||||
"useSceneName": bool,
|
||||
"quaternion": str, # "euler"
|
||||
"shapes": bool,
|
||||
"skins": bool,
|
||||
"constraints": bool,
|
||||
"lights": bool,
|
||||
"embeddedTextures": bool,
|
||||
"inputConnections": bool,
|
||||
"upAxis": str, # x, y or z,
|
||||
"triangulate": bool
|
||||
}
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""The default options for FBX extraction.
|
||||
|
||||
This includes shapes, skins, constraints, lights and incoming
|
||||
connections and exports with the Y-axis as up-axis.
|
||||
|
||||
By default this uses the time sliders start and end time.
|
||||
|
||||
"""
|
||||
|
||||
start_frame = int(cmds.playbackOptions(query=True,
|
||||
animationStartTime=True))
|
||||
end_frame = int(cmds.playbackOptions(query=True,
|
||||
animationEndTime=True))
|
||||
|
||||
return {
|
||||
"cameras": False,
|
||||
"smoothingGroups": False,
|
||||
"hardEdges": False,
|
||||
"tangents": False,
|
||||
"smoothMesh": False,
|
||||
"instances": False,
|
||||
"bakeComplexAnimation": True,
|
||||
"bakeComplexStart": start_frame,
|
||||
"bakeComplexEnd": end_frame,
|
||||
"bakeComplexStep": 1,
|
||||
"bakeResampleAnimation": True,
|
||||
"animationOnly": False,
|
||||
"useSceneName": False,
|
||||
"quaternion": "euler",
|
||||
"shapes": True,
|
||||
"skins": True,
|
||||
"constraints": False,
|
||||
"lights": True,
|
||||
"embeddedTextures": True,
|
||||
"inputConnections": True,
|
||||
"upAxis": "y",
|
||||
"triangulate": False
|
||||
}
|
||||
|
||||
def parse_overrides(self, instance, options):
|
||||
"""Inspect data of instance to determine overridden options
|
||||
|
||||
An instance may supply any of the overridable options
|
||||
as data, the option is then added to the extraction.
|
||||
|
||||
"""
|
||||
|
||||
for key in instance.data:
|
||||
if key not in self.options:
|
||||
continue
|
||||
|
||||
# Ensure the data is of correct type
|
||||
value = instance.data[key]
|
||||
if not isinstance(value, self.options[key]):
|
||||
self.log.warning(
|
||||
"Overridden attribute {key} was of "
|
||||
"the wrong type: {invalid_type} "
|
||||
"- should have been {valid_type}".format(
|
||||
key=key,
|
||||
invalid_type=type(value).__name__,
|
||||
valid_type=self.options[key].__name__))
|
||||
continue
|
||||
|
||||
options[key] = value
|
||||
|
||||
return options
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Ensure FBX plug-in is loaded
|
||||
cmds.loadPlugin("fbxmaya", quiet=True)
|
||||
fbx_exporter = fbx.FBXExtractor(log=self.log)
|
||||
|
||||
# Define output path
|
||||
stagingDir = self.staging_dir(instance)
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.fbx".format(instance.name)
|
||||
path = os.path.join(stagingDir, filename)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
# The export requires forward slashes because we need
|
||||
# to format it into a string in a mel expression
|
||||
|
|
@ -162,54 +39,13 @@ class ExtractFBX(openpype.api.Extractor):
|
|||
self.log.info("Members: {0}".format(members))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
# Parse export options
|
||||
options = self.default_options
|
||||
options = self.parse_overrides(instance, options)
|
||||
self.log.info("Export options: {0}".format(options))
|
||||
|
||||
# Collect the start and end including handles
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
|
||||
options['bakeComplexStart'] = start
|
||||
options['bakeComplexEnd'] = end
|
||||
|
||||
# First apply the default export settings to be fully consistent
|
||||
# each time for successive publishes
|
||||
mel.eval("FBXResetExport")
|
||||
|
||||
# Apply the FBX overrides through MEL since the commands
|
||||
# only work correctly in MEL according to online
|
||||
# available discussions on the topic
|
||||
_iteritems = getattr(options, "iteritems", options.items)
|
||||
for option, value in _iteritems():
|
||||
key = option[0].upper() + option[1:] # uppercase first letter
|
||||
|
||||
# Boolean must be passed as lower-case strings
|
||||
# as to MEL standards
|
||||
if isinstance(value, bool):
|
||||
value = str(value).lower()
|
||||
|
||||
template = "FBXExport{0} {1}" if key == "UpAxis" else "FBXExport{0} -v {1}" # noqa
|
||||
cmd = template.format(key, value)
|
||||
self.log.info(cmd)
|
||||
mel.eval(cmd)
|
||||
|
||||
# Never show the UI or generate a log
|
||||
mel.eval("FBXExportShowUI -v false")
|
||||
mel.eval("FBXExportGenerateLog -v false")
|
||||
fbx_exporter.set_options_from_instance(instance)
|
||||
|
||||
# Export
|
||||
if "unrealStaticMesh" in instance.data["families"]:
|
||||
with maintained_selection():
|
||||
with root_parent(members):
|
||||
self.log.info("Un-parenting: {}".format(members))
|
||||
cmds.select(members, r=1, noExpand=True)
|
||||
mel.eval('FBXExport -f "{}" -s'.format(path))
|
||||
else:
|
||||
with maintained_selection():
|
||||
cmds.select(members, r=1, noExpand=True)
|
||||
mel.eval('FBXExport -f "{}" -s'.format(path))
|
||||
with maintained_selection():
|
||||
fbx_exporter.export(members, path)
|
||||
cmds.select(members, r=1, noExpand=True)
|
||||
mel.eval('FBXExport -f "{}" -s'.format(path))
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -218,7 +54,7 @@ class ExtractFBX(openpype.api.Extractor):
|
|||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': filename,
|
||||
"stagingDir": stagingDir,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create Unreal Skeletal Mesh data to be extracted as FBX."""
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
from maya import cmds # noqa
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
@contextmanager
|
||||
def renamed(original_name, renamed_name):
|
||||
# type: (str, str) -> None
|
||||
try:
|
||||
cmds.rename(original_name, renamed_name)
|
||||
yield
|
||||
finally:
|
||||
cmds.rename(renamed_name, original_name)
|
||||
|
||||
|
||||
class ExtractUnrealSkeletalMesh(openpype.api.Extractor):
|
||||
"""Extract Unreal Skeletal Mesh as FBX from Maya. """
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Extract Unreal Skeletal Mesh"
|
||||
families = ["skeletalMesh"]
|
||||
|
||||
def process(self, instance):
|
||||
fbx_exporter = fbx.FBXExtractor(log=self.log)
|
||||
|
||||
# Define output path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.fbx".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
geo = instance.data.get("geometry")
|
||||
joints = instance.data.get("joints")
|
||||
|
||||
to_extract = geo + joints
|
||||
|
||||
# The export requires forward slashes because we need
|
||||
# to format it into a string in a mel expression
|
||||
path = path.replace('\\', '/')
|
||||
|
||||
self.log.info("Extracting FBX to: {0}".format(path))
|
||||
self.log.info("Members: {0}".format(to_extract))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
fbx_exporter.set_options_from_instance(instance)
|
||||
|
||||
# This magic is done for variants. To let Unreal merge correctly
|
||||
# existing data, top node must have the same name. So for every
|
||||
# variant we extract we need to rename top node of the rig correctly.
|
||||
# It is finally done in context manager so it won't affect current
|
||||
# scene.
|
||||
|
||||
# we rely on hierarchy under one root.
|
||||
original_parent = to_extract[0].split("|")[1]
|
||||
|
||||
parent_node = instance.data.get("asset")
|
||||
|
||||
renamed_to_extract = []
|
||||
for node in to_extract:
|
||||
node_path = node.split("|")
|
||||
node_path[1] = parent_node
|
||||
renamed_to_extract.append("|".join(node_path))
|
||||
|
||||
with renamed(original_parent, parent_node):
|
||||
self.log.info("Extracting: {}".format(renamed_to_extract, path))
|
||||
fbx_exporter.export(renamed_to_extract, path)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract FBX successful to: {0}".format(path))
|
||||
|
|
@ -1,33 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create Unreal Static Mesh data to be extracted as FBX."""
|
||||
import openpype.api
|
||||
import pyblish.api
|
||||
import os
|
||||
|
||||
from maya import cmds # noqa
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
parent_nodes,
|
||||
maintained_selection
|
||||
)
|
||||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
class ExtractUnrealStaticMesh(openpype.api.Extractor):
|
||||
"""Extract FBX from Maya. """
|
||||
"""Extract Unreal Static Mesh as FBX from Maya. """
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Extract Unreal Static Mesh"
|
||||
families = ["unrealStaticMesh"]
|
||||
families = ["staticMesh"]
|
||||
|
||||
def process(self, instance):
|
||||
to_combine = instance.data.get("membersToCombine")
|
||||
static_mesh_name = instance.data.get("staticMeshCombinedName")
|
||||
self.log.info(
|
||||
"merging {} into {}".format(
|
||||
" + ".join(to_combine), static_mesh_name))
|
||||
duplicates = cmds.duplicate(to_combine, ic=True)
|
||||
cmds.polyUnite(
|
||||
*duplicates,
|
||||
n=static_mesh_name, ch=False)
|
||||
members = instance.data.get("geometryMembers", [])
|
||||
if instance.data.get("collisionMembers"):
|
||||
members = members + instance.data.get("collisionMembers")
|
||||
|
||||
if not instance.data.get("cleanNodes"):
|
||||
instance.data["cleanNodes"] = []
|
||||
fbx_exporter = fbx.FBXExtractor(log=self.log)
|
||||
|
||||
instance.data["cleanNodes"].append(static_mesh_name)
|
||||
instance.data["cleanNodes"] += duplicates
|
||||
# Define output path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.fbx".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
instance.data["setMembers"] = [static_mesh_name]
|
||||
instance.data["setMembers"] += instance.data["collisionMembers"]
|
||||
# The export requires forward slashes because we need
|
||||
# to format it into a string in a mel expression
|
||||
path = path.replace('\\', '/')
|
||||
|
||||
self.log.info("Extracting FBX to: {0}".format(path))
|
||||
self.log.info("Members: {0}".format(members))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
fbx_exporter.set_options_from_instance(instance)
|
||||
|
||||
with maintained_selection():
|
||||
with parent_nodes(members):
|
||||
self.log.info("Un-parenting: {}".format(members))
|
||||
fbx_exporter.export(members, path)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract FBX successful to: {0}".format(path))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Skeletal Mesh Top Node</title>
|
||||
<description>## Skeletal meshes needs common root
|
||||
|
||||
Skeletal meshes and their joints must be under one common root.
|
||||
|
||||
### How to repair?
|
||||
|
||||
Make sure all geometry and joints resides under same root.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin):
|
||||
"""Validates that nodes has common root."""
|
||||
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
hosts = ["maya"]
|
||||
families = ["skeletalMesh"]
|
||||
label = "Skeletal Mesh Top Node"
|
||||
|
||||
def process(self, instance):
|
||||
geo = instance.data.get("geometry")
|
||||
joints = instance.data.get("joints")
|
||||
|
||||
joints_parents = cmds.ls(joints, long=True)
|
||||
geo_parents = cmds.ls(geo, long=True)
|
||||
|
||||
parents_set = {
|
||||
parent.split("|")[1] for parent in (joints_parents + geo_parents)
|
||||
}
|
||||
|
||||
if len(set(parents_set)) != 1:
|
||||
raise PublishXmlValidationError(
|
||||
self,
|
||||
"Multiple roots on geometry or joints."
|
||||
)
|
||||
|
|
@ -10,10 +10,11 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
|
|||
|
||||
order = openpype.api.ValidateMeshOrder
|
||||
hosts = ["maya"]
|
||||
families = ["unrealStaticMesh"]
|
||||
families = ["staticMesh"]
|
||||
category = "geometry"
|
||||
label = "Mesh is Triangulated"
|
||||
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
|
||||
active = False
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Validator for correct naming of Static Meshes."""
|
||||
from maya import cmds # noqa
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
|
@ -52,8 +52,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
optional = True
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
hosts = ["maya"]
|
||||
families = ["unrealStaticMesh"]
|
||||
label = "Unreal StaticMesh Name"
|
||||
families = ["staticMesh"]
|
||||
label = "Unreal Static Mesh Name"
|
||||
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
|
||||
regex_mesh = r"(?P<renderName>.*))"
|
||||
regex_collision = r"(?P<renderName>.*)"
|
||||
|
|
@ -72,15 +72,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
["collision_prefixes"]
|
||||
)
|
||||
|
||||
combined_geometry_name = instance.data.get(
|
||||
"staticMeshCombinedName", None)
|
||||
if cls.validate_mesh:
|
||||
# compile regex for testing names
|
||||
regex_mesh = "{}{}".format(
|
||||
("_" + cls.static_mesh_prefix) or "", cls.regex_mesh
|
||||
)
|
||||
sm_r = re.compile(regex_mesh)
|
||||
if not sm_r.match(combined_geometry_name):
|
||||
if not sm_r.match(instance.data.get("subset")):
|
||||
cls.log.error("Mesh doesn't comply with name validation.")
|
||||
return True
|
||||
|
||||
|
|
@ -91,7 +89,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
cls.log.warning("No collision objects to validate.")
|
||||
return False
|
||||
|
||||
regex_collision = "{}{}".format(
|
||||
regex_collision = "{}{}_(\\d+)".format(
|
||||
"(?P<prefix>({}))_".format(
|
||||
"|".join("{0}".format(p) for p in collision_prefixes)
|
||||
) or "", cls.regex_collision
|
||||
|
|
@ -99,6 +97,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
|
||||
cl_r = re.compile(regex_collision)
|
||||
|
||||
mesh_name = "{}{}".format(instance.data["asset"],
|
||||
instance.data.get("variant", []))
|
||||
|
||||
for obj in collision_set:
|
||||
cl_m = cl_r.match(obj)
|
||||
if not cl_m:
|
||||
|
|
@ -107,7 +108,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
expected_collision = "{}_{}".format(
|
||||
cl_m.group("prefix"),
|
||||
combined_geometry_name
|
||||
mesh_name
|
||||
)
|
||||
|
||||
if not obj.startswith(expected_collision):
|
||||
|
|
@ -116,11 +117,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
|
|||
"Collision object name doesn't match "
|
||||
"static mesh name"
|
||||
)
|
||||
cls.log.error("{}_{} != {}_{}".format(
|
||||
cls.log.error("{}_{} != {}_{}*".format(
|
||||
cl_m.group("prefix"),
|
||||
cl_m.group("renderName"),
|
||||
cl_m.group("prefix"),
|
||||
combined_geometry_name,
|
||||
mesh_name,
|
||||
))
|
||||
invalid.append(obj)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin):
|
|||
"""Validate if Z is set as up axis in Maya"""
|
||||
|
||||
optional = True
|
||||
active = False
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
hosts = ["maya"]
|
||||
families = ["unrealStaticMesh"]
|
||||
families = ["staticMesh"]
|
||||
label = "Unreal Up-Axis check"
|
||||
actions = [openpype.api.RepairAction]
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_nuke_path = env.get("NUKE_PATH") or ""
|
||||
for path in old_nuke_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -1048,17 +1048,28 @@ def add_review_knob(node):
|
|||
def add_deadline_tab(node):
|
||||
node.addKnob(nuke.Tab_Knob("Deadline"))
|
||||
|
||||
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlinePriority", "Priority")
|
||||
knob.setValue(50)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks")
|
||||
# zero as default will get value from Settings during collection
|
||||
# instead of being an explicit user override, see precollect_write.py
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
|
||||
def get_deadline_knob_names():
|
||||
return ["Deadline", "deadlineChunkSize", "deadlinePriority"]
|
||||
return [
|
||||
"Deadline",
|
||||
"deadlineChunkSize",
|
||||
"deadlinePriority",
|
||||
"deadlineConcurrentTasks"
|
||||
]
|
||||
|
||||
|
||||
def create_backdrop(label="", color=None, layer=0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
import nuke
|
||||
import six
|
||||
|
||||
from avalon import io
|
||||
|
||||
|
|
@ -333,7 +334,7 @@ class LoadEffects(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, str):
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
import nuke
|
||||
|
||||
from avalon import io
|
||||
|
|
@ -353,7 +353,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, str):
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import nuke
|
||||
|
||||
import six
|
||||
from avalon import io
|
||||
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -243,8 +243,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, unicode):
|
||||
return input.encode('utf-8')
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
|
|||
if generated_repres:
|
||||
# assign to representations
|
||||
instance.data["representations"] += generated_repres
|
||||
instance.data["hasReviewableRepresentations"] = True
|
||||
else:
|
||||
instance.data["families"].remove("review")
|
||||
self.log.info((
|
||||
|
|
|
|||
|
|
@ -128,13 +128,17 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
}
|
||||
|
||||
group_node = [x for x in instance if x.Class() == "Group"][0]
|
||||
deadlineChunkSize = 1
|
||||
dl_chunk_size = 1
|
||||
if "deadlineChunkSize" in group_node.knobs():
|
||||
deadlineChunkSize = group_node["deadlineChunkSize"].value()
|
||||
dl_chunk_size = group_node["deadlineChunkSize"].value()
|
||||
|
||||
deadlinePriority = 50
|
||||
dl_priority = 50
|
||||
if "deadlinePriority" in group_node.knobs():
|
||||
deadlinePriority = group_node["deadlinePriority"].value()
|
||||
dl_priority = group_node["deadlinePriority"].value()
|
||||
|
||||
dl_concurrent_tasks = 0
|
||||
if "deadlineConcurrentTasks" in group_node.knobs():
|
||||
dl_concurrent_tasks = group_node["deadlineConcurrentTasks"].value()
|
||||
|
||||
instance.data.update({
|
||||
"versionData": version_data,
|
||||
|
|
@ -144,8 +148,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
"label": label,
|
||||
"outputType": output_type,
|
||||
"colorspace": colorspace,
|
||||
"deadlineChunkSize": deadlineChunkSize,
|
||||
"deadlinePriority": deadlinePriority
|
||||
"deadlineChunkSize": dl_chunk_size,
|
||||
"deadlinePriority": dl_priority,
|
||||
"deadlineConcurrentTasks": dl_concurrent_tasks
|
||||
})
|
||||
|
||||
if self.is_prerender(_families_test):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect original base name for use in templates."""
|
||||
from pathlib import Path
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectOriginalBasename(pyblish.api.InstancePlugin):
|
||||
"""Collect original file base name."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.498
|
||||
label = "Collect Base Name"
|
||||
hosts = ["standalonepublisher"]
|
||||
families = ["simpleUnrealTexture"]
|
||||
|
||||
def process(self, instance):
|
||||
file_name = Path(instance.data["representations"][0]["files"])
|
||||
instance.data["originalBasename"] = file_name.stem
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Invalid texture name</title>
|
||||
<description>
|
||||
## Invalid file name
|
||||
|
||||
Submitted file has invalid name:
|
||||
'{invalid_file}'
|
||||
|
||||
### How to repair?
|
||||
|
||||
Texture file must adhere to naming conventions for Unreal:
|
||||
T_{asset}_*.ext
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator for correct file naming."""
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
import re
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
|
||||
|
||||
class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin):
|
||||
label = "Validate Unreal Texture Names"
|
||||
hosts = ["standalonepublisher"]
|
||||
families = ["simpleUnrealTexture"]
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
regex = "^T_{asset}.*"
|
||||
|
||||
def process(self, instance):
|
||||
file_name = instance.data.get("originalBasename")
|
||||
self.log.info(file_name)
|
||||
pattern = self.regex.format(asset=instance.data.get("asset"))
|
||||
if not re.match(pattern, file_name):
|
||||
msg = f"Invalid file name {file_name}"
|
||||
raise PublishXmlValidationError(
|
||||
self, msg, formatting_data={
|
||||
"invalid_file": file_name,
|
||||
"asset": instance.data.get("asset")
|
||||
})
|
||||
|
|
@ -7,7 +7,7 @@ from avalon import io
|
|||
import avalon.api
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import BaseCreator
|
||||
from openpype.pipeline import register_creator_plugin_path
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)
|
||||
|
|
@ -169,7 +169,7 @@ def install():
|
|||
|
||||
pyblish.api.register_host("traypublisher")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.register_plugin_path(BaseCreator, CREATE_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
|
||||
|
||||
def set_project_name(project_name):
|
||||
|
|
|
|||
|
|
@ -14,11 +14,22 @@ class ValidateWorkfilePath(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
filepath = instance.data["sourceFilepath"]
|
||||
if not filepath:
|
||||
raise PublishValidationError((
|
||||
"Filepath of 'workfile' instance \"{}\" is not set"
|
||||
).format(instance.data["name"]))
|
||||
raise PublishValidationError(
|
||||
(
|
||||
"Filepath of 'workfile' instance \"{}\" is not set"
|
||||
).format(instance.data["name"]),
|
||||
"File not filled",
|
||||
"## Missing file\nYou are supposed to fill the path."
|
||||
)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
raise PublishValidationError((
|
||||
"Filepath of 'workfile' instance \"{}\" does not exist: {}"
|
||||
).format(instance.data["name"], filepath))
|
||||
raise PublishValidationError(
|
||||
(
|
||||
"Filepath of 'workfile' instance \"{}\" does not exist: {}"
|
||||
).format(instance.data["name"], filepath),
|
||||
"File not found",
|
||||
(
|
||||
"## File was not found\nFile \"{}\" was not found."
|
||||
" Check if the path is still available."
|
||||
).format(filepath)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
# presets
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
concurrent_tasks = 1
|
||||
primary_pool = ""
|
||||
secondary_pool = ""
|
||||
group = ""
|
||||
|
|
@ -149,11 +150,16 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
pass
|
||||
|
||||
# define chunk and priority
|
||||
chunk_size = instance.data.get("deadlineChunkSize")
|
||||
chunk_size = instance.data["deadlineChunkSize"]
|
||||
if chunk_size == 0 and self.chunk_size:
|
||||
chunk_size = self.chunk_size
|
||||
|
||||
priority = instance.data.get("deadlinePriority")
|
||||
# define chunk and priority
|
||||
concurrent_tasks = instance.data["deadlineConcurrentTasks"]
|
||||
if concurrent_tasks == 0 and self.concurrent_tasks:
|
||||
concurrent_tasks = self.concurrent_tasks
|
||||
|
||||
priority = instance.data["deadlinePriority"]
|
||||
if not priority:
|
||||
priority = self.priority
|
||||
|
||||
|
|
@ -177,6 +183,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
"Priority": priority,
|
||||
"ChunkSize": chunk_size,
|
||||
"ConcurrentTasks": concurrent_tasks,
|
||||
|
||||
"Department": self.department,
|
||||
|
||||
"Pool": self.primary_pool,
|
||||
|
|
|
|||
|
|
@ -509,8 +509,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
most cases, but if not - we create representation from each of them.
|
||||
|
||||
Arguments:
|
||||
instance (pyblish.plugin.Instance): instance for which we are
|
||||
setting representations
|
||||
instance (dict): instance data for which we are
|
||||
setting representations
|
||||
exp_files (list): list of expected files
|
||||
|
||||
Returns:
|
||||
|
|
@ -528,6 +528,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
# preview video rendering
|
||||
for app in self.aov_filter.keys():
|
||||
if os.environ.get("AVALON_APP", "") == app:
|
||||
# no need to add review if `hasReviewableRepresentations`
|
||||
if instance.get("hasReviewableRepresentations"):
|
||||
break
|
||||
|
||||
# iteratre all aov filters
|
||||
for aov in self.aov_filter[app]:
|
||||
if re.match(
|
||||
aov,
|
||||
|
|
@ -724,7 +729,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
"resolutionWidth": data.get("resolutionWidth", 1920),
|
||||
"resolutionHeight": data.get("resolutionHeight", 1080),
|
||||
"multipartExr": data.get("multipartExr", False),
|
||||
"jobBatchName": data.get("jobBatchName", "")
|
||||
"jobBatchName": data.get("jobBatchName", ""),
|
||||
"hasReviewableRepresentations": data.get(
|
||||
"hasReviewableRepresentations")
|
||||
}
|
||||
|
||||
if "prerender" in instance.data["families"]:
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
"image": "img",
|
||||
"reference": "reference"
|
||||
}
|
||||
keep_first_subset_name_for_review = True
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("instance {}".format(instance))
|
||||
|
|
@ -168,7 +169,47 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
# Change asset name of each new component for review
|
||||
is_first_review_repre = True
|
||||
not_first_components = []
|
||||
extended_asset_name = ""
|
||||
multiple_reviewable = len(review_representations) > 1
|
||||
for repre in review_representations:
|
||||
# Create copy of base comp item and append it
|
||||
review_item = copy.deepcopy(base_component_item)
|
||||
|
||||
# get asset name and define extended name variant
|
||||
asset_name = review_item["asset_data"]["name"]
|
||||
extended_asset_name = "_".join(
|
||||
(asset_name, repre["name"])
|
||||
)
|
||||
|
||||
# reset extended if no need for extended asset name
|
||||
if (
|
||||
self.keep_first_subset_name_for_review
|
||||
and is_first_review_repre
|
||||
):
|
||||
extended_asset_name = ""
|
||||
else:
|
||||
# only rename if multiple reviewable
|
||||
if multiple_reviewable:
|
||||
review_item["asset_data"]["name"] = extended_asset_name
|
||||
else:
|
||||
extended_asset_name = ""
|
||||
|
||||
# rename all already created components
|
||||
# only if first repre and extended name available
|
||||
if is_first_review_repre and extended_asset_name:
|
||||
# and rename all already created components
|
||||
for _ci in component_list:
|
||||
_ci["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
# and rename all already created src components
|
||||
for _sci in src_components_to_add:
|
||||
_sci["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
# rename also first thumbnail component if any
|
||||
if first_thumbnail_component is not None:
|
||||
first_thumbnail_component[
|
||||
"asset_data"]["name"] = extended_asset_name
|
||||
|
||||
frame_start = repre.get("frameStartFtrack")
|
||||
frame_end = repre.get("frameEndFtrack")
|
||||
if frame_start is None or frame_end is None:
|
||||
|
|
@ -184,8 +225,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
if fps is None:
|
||||
fps = instance_fps
|
||||
|
||||
# Create copy of base comp item and append it
|
||||
review_item = copy.deepcopy(base_component_item)
|
||||
# Change location
|
||||
review_item["component_path"] = repre["published_path"]
|
||||
# Change component data
|
||||
|
|
@ -200,18 +239,16 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
})
|
||||
}
|
||||
}
|
||||
# Create copy of item before setting location or changing asset
|
||||
src_components_to_add.append(copy.deepcopy(review_item))
|
||||
|
||||
if is_first_review_repre:
|
||||
is_first_review_repre = False
|
||||
else:
|
||||
# Add representation name to asset name of "not first" review
|
||||
asset_name = review_item["asset_data"]["name"]
|
||||
review_item["asset_data"]["name"] = "_".join(
|
||||
(asset_name, repre["name"])
|
||||
)
|
||||
# later detection for thumbnail duplication
|
||||
not_first_components.append(review_item)
|
||||
|
||||
# Create copy of item before setting location
|
||||
src_components_to_add.append(copy.deepcopy(review_item))
|
||||
|
||||
# Set location
|
||||
review_item["component_location"] = ftrack_server_location
|
||||
# Add item to component list
|
||||
|
|
@ -249,6 +286,14 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
continue
|
||||
# Create copy of base comp item and append it
|
||||
other_item = copy.deepcopy(base_component_item)
|
||||
|
||||
# add extended name if any
|
||||
if (
|
||||
not self.keep_first_subset_name_for_review
|
||||
and extended_asset_name
|
||||
):
|
||||
other_item["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
other_item["component_data"] = {
|
||||
"name": repre["name"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"textures",
|
||||
"action",
|
||||
"background",
|
||||
"effect"
|
||||
"effect",
|
||||
"staticMesh",
|
||||
"skeletalMesh"
|
||||
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -485,6 +485,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
anatomy = instance.context.data["anatomy"]
|
||||
template_data = copy.deepcopy(instance.data["anatomyData"])
|
||||
|
||||
if "originalBasename" in instance.data:
|
||||
template_data.update({
|
||||
"originalBasename": instance.data.get("originalBasename")
|
||||
})
|
||||
|
||||
if "folder" in anatomy.templates[template_key]:
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
publish_folder = anatomy_filled[template_key]["folder"]
|
||||
|
|
|
|||
|
|
@ -106,8 +106,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"xgen",
|
||||
"hda",
|
||||
"usd",
|
||||
"staticMesh",
|
||||
"skeletalMesh",
|
||||
"usdComposition",
|
||||
"usdOverride"
|
||||
"usdOverride",
|
||||
"simpleUnrealTexture"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
db_representation_context_keys = [
|
||||
|
|
@ -355,6 +358,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
if profile:
|
||||
template_name = profile["template_name"]
|
||||
|
||||
|
||||
|
||||
published_representations = {}
|
||||
for idx, repre in enumerate(instance.data["representations"]):
|
||||
# reset transfers for next representation
|
||||
|
|
@ -383,6 +388,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
if resolution_width:
|
||||
template_data["fps"] = fps
|
||||
|
||||
if "originalBasename" in instance.data:
|
||||
template_data.update({
|
||||
"originalBasename": instance.data.get("originalBasename")
|
||||
})
|
||||
|
||||
files = repre['files']
|
||||
if repre.get('stagingDir'):
|
||||
stagingdir = repre['stagingDir']
|
||||
|
|
@ -554,6 +564,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
repre['published_path'] = dst
|
||||
self.log.debug("__ dst: {}".format(dst))
|
||||
|
||||
if not instance.data.get("publishDir"):
|
||||
instance.data["publishDir"] = (
|
||||
anatomy_filled
|
||||
[template_name]
|
||||
["folder"]
|
||||
)
|
||||
if repre.get("udim"):
|
||||
repre_context["udim"] = repre.get("udim") # store list
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,30 @@
|
|||
},
|
||||
"delivery": {},
|
||||
"unreal": {
|
||||
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}",
|
||||
"file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
|
||||
"folder": "{root[work]}/{project[name]}/unreal/{task[name]}",
|
||||
"file": "{project[code]}_{asset}",
|
||||
"path": "{@folder}/{@file}"
|
||||
},
|
||||
"others": {}
|
||||
"others": {
|
||||
"maya2unreal": {
|
||||
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}",
|
||||
"file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
|
||||
"path": "{@folder}/{@file}"
|
||||
},
|
||||
"simpleUnrealTextureHero": {
|
||||
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/hero",
|
||||
"file": "{originalBasename}.{ext}",
|
||||
"path": "{@folder}/{@file}"
|
||||
},
|
||||
"simpleUnrealTexture": {
|
||||
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{@version}",
|
||||
"file": "{originalBasename}_{@version}.{ext}",
|
||||
"path": "{@folder}/{@file}"
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"maya2unreal": "Maya to Unreal",
|
||||
"simpleUnrealTextureHero": "Simple Unreal Texture - Hero",
|
||||
"simpleUnrealTexture": "Simple Unreal Texture"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
"use_published": true,
|
||||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"concurrent_tasks": 1,
|
||||
"primary_pool": "",
|
||||
"secondary_pool": "",
|
||||
"group": "",
|
||||
|
|
|
|||
|
|
@ -395,7 +395,8 @@
|
|||
"vrayproxy": "cache",
|
||||
"redshiftproxy": "cache",
|
||||
"usd": "usd"
|
||||
}
|
||||
},
|
||||
"keep_first_subset_name_for_review": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,6 +178,29 @@
|
|||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template_name": "render"
|
||||
},
|
||||
{
|
||||
"families": [
|
||||
"simpleUnrealTexture"
|
||||
],
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template_name": "simpleUnrealTexture"
|
||||
},
|
||||
{
|
||||
"families": [
|
||||
"staticMesh",
|
||||
"skeletalMesh"
|
||||
],
|
||||
"hosts": [
|
||||
"maya"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template_name": "maya2unreal"
|
||||
}
|
||||
],
|
||||
"subset_grouping_profiles": [
|
||||
|
|
@ -202,9 +225,22 @@
|
|||
"animation",
|
||||
"setdress",
|
||||
"layout",
|
||||
"mayaScene"
|
||||
"mayaScene",
|
||||
"simpleUnrealTexture"
|
||||
],
|
||||
"template_name_profiles": []
|
||||
"template_name_profiles": [
|
||||
{
|
||||
"families": [
|
||||
"simpleUnrealTexture"
|
||||
],
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"template_name": "simpleUnrealTextureHero"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CleanUp": {
|
||||
"paterns": [],
|
||||
|
|
@ -289,7 +325,7 @@
|
|||
},
|
||||
{
|
||||
"families": [
|
||||
"unrealStaticMesh"
|
||||
"staticMesh"
|
||||
],
|
||||
"hosts": [
|
||||
"maya"
|
||||
|
|
@ -297,6 +333,17 @@
|
|||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "S_{asset}{variant}"
|
||||
},
|
||||
{
|
||||
"families": [
|
||||
"skeletalMesh"
|
||||
],
|
||||
"hosts": [
|
||||
"maya"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "SK_{asset}{variant}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -306,6 +353,13 @@
|
|||
"task_types": [],
|
||||
"hosts": [],
|
||||
"workfile_template": "work"
|
||||
},
|
||||
{
|
||||
"task_types": [],
|
||||
"hosts": [
|
||||
"unreal"
|
||||
],
|
||||
"workfile_template": "unreal"
|
||||
}
|
||||
],
|
||||
"last_workfile_on_startup": [
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
"",
|
||||
"_Main"
|
||||
],
|
||||
"static_mesh_prefix": "S_",
|
||||
"static_mesh_prefix": "S",
|
||||
"collision_prefixes": [
|
||||
"UBX",
|
||||
"UCP",
|
||||
|
|
@ -60,6 +60,11 @@
|
|||
"UCX"
|
||||
]
|
||||
},
|
||||
"CreateUnrealSkeletalMesh": {
|
||||
"enabled": true,
|
||||
"defaults": [],
|
||||
"joint_hints": "jnt_org"
|
||||
},
|
||||
"CreateAnimation": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
|
|||
|
|
@ -133,6 +133,14 @@
|
|||
],
|
||||
"help": "Texture files with UDIM together with worfile"
|
||||
},
|
||||
"create_simple_unreal_texture": {
|
||||
"name": "simple_unreal_texture",
|
||||
"label": "Simple Unreal Texture",
|
||||
"family": "simpleUnrealTexture",
|
||||
"icon": "Image",
|
||||
"defaults": [],
|
||||
"help": "Texture files with Unreal naming convention"
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"create_workfile": "Workfile",
|
||||
"create_model": "Model",
|
||||
|
|
@ -145,7 +153,8 @@
|
|||
"create_matchmove": "Matchmove",
|
||||
"create_render": "Render",
|
||||
"create_mov_batch": "Batch Mov",
|
||||
"create_texture_batch": "Batch Texture"
|
||||
"create_texture_batch": "Batch Texture",
|
||||
"create_simple_unreal_texture": "Simple Unreal Texture"
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,9 @@
|
|||
"key": "use_published",
|
||||
"label": "Use Published scene"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "priority",
|
||||
|
|
@ -202,6 +205,14 @@
|
|||
"key": "chunk_size",
|
||||
"label": "Chunk Size"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "concurrent_tasks",
|
||||
"label": "Number of concurrent tasks"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "primary_pool",
|
||||
|
|
@ -217,6 +228,9 @@
|
|||
"key": "group",
|
||||
"label": "Group"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "department",
|
||||
|
|
|
|||
|
|
@ -784,6 +784,12 @@
|
|||
"object_type": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "keep_first_subset_name_for_review",
|
||||
"label": "Make subset name as first asset name",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,32 @@
|
|||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateUnrealSkeletalMesh",
|
||||
"label": "Create Unreal - Skeletal Mesh",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "joint_hints",
|
||||
"label": "Joint root hint"
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
background: #21252B;
|
||||
}
|
||||
|
||||
/* Workfiles */
|
||||
#WorkfilesPublishedContextSelect {
|
||||
background: rgba(0, 0, 0, 127);
|
||||
}
|
||||
#WorkfilesPublishedContextSelect QLabel {
|
||||
font-size: 17pt;
|
||||
}
|
||||
|
||||
/* Tray */
|
||||
#TrayRestartButton {
|
||||
background: {color:restart-btn-bg};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from .model import (
|
|||
DATE_MODIFIED_ROLE,
|
||||
)
|
||||
from .save_as_dialog import SaveAsDialog
|
||||
from .lib import TempPublishFiles
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -45,11 +44,35 @@ class FilesView(QtWidgets.QTreeView):
|
|||
return super(FilesView, self).mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
class SelectContextOverlay(QtWidgets.QFrame):
|
||||
def __init__(self, parent):
|
||||
super(SelectContextOverlay, self).__init__(parent)
|
||||
|
||||
self.setObjectName("WorkfilesPublishedContextSelect")
|
||||
label_widget = QtWidgets.QLabel(
|
||||
"Please choose context on the left<br/><",
|
||||
self
|
||||
)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
parent.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.Resize:
|
||||
self.resize(obj.size())
|
||||
|
||||
return super(SelectContextOverlay, self).eventFilter(obj, event)
|
||||
|
||||
|
||||
class FilesWidget(QtWidgets.QWidget):
|
||||
"""A widget displaying files that allows to save and open files."""
|
||||
file_selected = QtCore.Signal(str)
|
||||
file_opened = QtCore.Signal()
|
||||
publish_file_viewed = QtCore.Signal()
|
||||
workfile_created = QtCore.Signal(str)
|
||||
published_visible_changed = QtCore.Signal(bool)
|
||||
|
||||
|
|
@ -71,9 +94,6 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._workfiles_root = None
|
||||
self._workdir_path = None
|
||||
self.host = api.registered_host()
|
||||
temp_publish_files = TempPublishFiles()
|
||||
temp_publish_files.cleanup()
|
||||
self._temp_publish_files = temp_publish_files
|
||||
|
||||
# Whether to automatically select the latest modified
|
||||
# file on a refresh of the files model.
|
||||
|
|
@ -93,14 +113,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
filter_layout = QtWidgets.QHBoxLayout(filter_widget)
|
||||
filter_layout.setContentsMargins(0, 0, 0, 0)
|
||||
filter_layout.addWidget(published_checkbox, 0)
|
||||
filter_layout.addWidget(filter_input, 1)
|
||||
filter_layout.addWidget(published_checkbox, 0)
|
||||
|
||||
# Create the Files models
|
||||
extensions = set(self.host.file_extensions())
|
||||
|
||||
views_widget = QtWidgets.QWidget(self)
|
||||
# Workarea view
|
||||
# --- Workarea view ---
|
||||
workarea_files_model = WorkAreaFilesModel(extensions)
|
||||
|
||||
# Create proxy model for files to be able sort and filter
|
||||
|
|
@ -118,13 +138,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Date modified delegate
|
||||
workarea_time_delegate = PrettyTimeDelegate()
|
||||
workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate)
|
||||
workarea_files_view.setIndentation(3) # smaller indentation
|
||||
# smaller indentation
|
||||
workarea_files_view.setIndentation(3)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
workarea_files_view.setColumnWidth(0, 330)
|
||||
|
||||
# Publish files view
|
||||
# --- Publish files view ---
|
||||
publish_files_model = PublishFilesModel(extensions, io, self.anatomy)
|
||||
|
||||
publish_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
|
|
@ -141,12 +162,16 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Date modified delegate
|
||||
publish_time_delegate = PrettyTimeDelegate()
|
||||
publish_files_view.setItemDelegateForColumn(1, publish_time_delegate)
|
||||
publish_files_view.setIndentation(3) # smaller indentation
|
||||
# smaller indentation
|
||||
publish_files_view.setIndentation(3)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
publish_files_view.setColumnWidth(0, 330)
|
||||
|
||||
publish_context_overlay = SelectContextOverlay(views_widget)
|
||||
publish_context_overlay.setVisible(False)
|
||||
|
||||
views_layout = QtWidgets.QHBoxLayout(views_widget)
|
||||
views_layout.setContentsMargins(0, 0, 0, 0)
|
||||
views_layout.addWidget(workarea_files_view, 1)
|
||||
|
|
@ -155,18 +180,43 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Home Page
|
||||
# Build buttons widget for files widget
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
btn_save = QtWidgets.QPushButton("Save As", btns_widget)
|
||||
btn_browse = QtWidgets.QPushButton("Browse", btns_widget)
|
||||
btn_open = QtWidgets.QPushButton("Open", btns_widget)
|
||||
|
||||
btn_view_published = QtWidgets.QPushButton("View", btns_widget)
|
||||
workarea_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
btn_save = QtWidgets.QPushButton("Save As", workarea_btns_widget)
|
||||
btn_browse = QtWidgets.QPushButton("Browse", workarea_btns_widget)
|
||||
btn_open = QtWidgets.QPushButton("Open", workarea_btns_widget)
|
||||
|
||||
workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)
|
||||
workarea_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
workarea_btns_layout.addWidget(btn_open, 1)
|
||||
workarea_btns_layout.addWidget(btn_browse, 1)
|
||||
workarea_btns_layout.addWidget(btn_save, 1)
|
||||
|
||||
publish_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
btn_save_as_published = QtWidgets.QPushButton(
|
||||
"Copy && Open", publish_btns_widget
|
||||
)
|
||||
btn_change_context = QtWidgets.QPushButton(
|
||||
"Choose different context", publish_btns_widget
|
||||
)
|
||||
btn_select_context_published = QtWidgets.QPushButton(
|
||||
"Copy && Open", publish_btns_widget
|
||||
)
|
||||
btn_cancel_published = QtWidgets.QPushButton(
|
||||
"Cancel", publish_btns_widget
|
||||
)
|
||||
|
||||
publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget)
|
||||
publish_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
publish_btns_layout.addWidget(btn_save_as_published, 1)
|
||||
publish_btns_layout.addWidget(btn_change_context, 1)
|
||||
publish_btns_layout.addWidget(btn_select_context_published, 1)
|
||||
publish_btns_layout.addWidget(btn_cancel_published, 1)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addWidget(btn_open, 1)
|
||||
btns_layout.addWidget(btn_browse, 1)
|
||||
btns_layout.addWidget(btn_save, 1)
|
||||
btns_layout.addWidget(btn_view_published, 1)
|
||||
btns_layout.addWidget(workarea_btns_widget, 1)
|
||||
btns_layout.addWidget(publish_btns_widget, 1)
|
||||
|
||||
# Build files widgets for home page
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -188,14 +238,22 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
workarea_files_view.selectionModel().selectionChanged.connect(
|
||||
self.on_file_select
|
||||
)
|
||||
publish_files_view.doubleClickedLeft.connect(
|
||||
self._on_view_published_pressed
|
||||
)
|
||||
|
||||
btn_open.pressed.connect(self._on_workarea_open_pressed)
|
||||
btn_browse.pressed.connect(self.on_browse_pressed)
|
||||
btn_save.pressed.connect(self.on_save_as_pressed)
|
||||
btn_view_published.pressed.connect(self._on_view_published_pressed)
|
||||
btn_save.pressed.connect(self._on_save_as_pressed)
|
||||
btn_save_as_published.pressed.connect(
|
||||
self._on_published_save_as_pressed
|
||||
)
|
||||
btn_change_context.pressed.connect(
|
||||
self._on_publish_change_context_pressed
|
||||
)
|
||||
btn_select_context_published.pressed.connect(
|
||||
self._on_publish_select_context_pressed
|
||||
)
|
||||
btn_cancel_published.pressed.connect(
|
||||
self._on_publish_cancel_pressed
|
||||
)
|
||||
|
||||
# Store attributes
|
||||
self._published_checkbox = published_checkbox
|
||||
|
|
@ -211,18 +269,29 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._publish_files_model = publish_files_model
|
||||
self._publish_proxy_model = publish_proxy_model
|
||||
|
||||
self._btns_widget = btns_widget
|
||||
self._publish_context_overlay = publish_context_overlay
|
||||
|
||||
self._workarea_btns_widget = workarea_btns_widget
|
||||
self._publish_btns_widget = publish_btns_widget
|
||||
self._btn_open = btn_open
|
||||
self._btn_browse = btn_browse
|
||||
self._btn_save = btn_save
|
||||
self._btn_view_published = btn_view_published
|
||||
|
||||
self._btn_save_as_published = btn_save_as_published
|
||||
self._btn_change_context = btn_change_context
|
||||
self._btn_select_context_published = btn_select_context_published
|
||||
self._btn_cancel_published = btn_cancel_published
|
||||
|
||||
# Create a proxy widget for files widget
|
||||
self.setFocusProxy(btn_open)
|
||||
|
||||
# Hide publish files widgets
|
||||
publish_files_view.setVisible(False)
|
||||
btn_view_published.setVisible(False)
|
||||
publish_btns_widget.setVisible(False)
|
||||
btn_select_context_published.setVisible(False)
|
||||
btn_cancel_published.setVisible(False)
|
||||
|
||||
self._publish_context_select_mode = False
|
||||
|
||||
@property
|
||||
def published_enabled(self):
|
||||
|
|
@ -232,12 +301,10 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
published_enabled = self.published_enabled
|
||||
|
||||
self._workarea_files_view.setVisible(not published_enabled)
|
||||
self._btn_open.setVisible(not published_enabled)
|
||||
self._btn_browse.setVisible(not published_enabled)
|
||||
self._btn_save.setVisible(not published_enabled)
|
||||
self._workarea_btns_widget.setVisible(not published_enabled)
|
||||
|
||||
self._publish_files_view.setVisible(published_enabled)
|
||||
self._btn_view_published.setVisible(published_enabled)
|
||||
self._publish_btns_widget.setVisible(published_enabled)
|
||||
|
||||
self._update_filtering()
|
||||
self._update_asset_task()
|
||||
|
|
@ -258,6 +325,9 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_save_enabled(self, enabled):
|
||||
self._btn_save.setEnabled(enabled)
|
||||
if not enabled and self._published_checkbox.isChecked():
|
||||
self._published_checkbox.setChecked(False)
|
||||
self._published_checkbox.setVisible(enabled)
|
||||
|
||||
def set_asset_task(self, asset_id, task_name, task_type):
|
||||
if asset_id != self._asset_id:
|
||||
|
|
@ -268,12 +338,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._update_asset_task()
|
||||
|
||||
def _update_asset_task(self):
|
||||
if self.published_enabled:
|
||||
if self.published_enabled and not self._publish_context_select_mode:
|
||||
self._publish_files_model.set_context(
|
||||
self._asset_id, self._task_name
|
||||
)
|
||||
has_valid_items = self._publish_files_model.has_valid_items()
|
||||
self._btn_view_published.setEnabled(has_valid_items)
|
||||
self._btn_save_as_published.setEnabled(has_valid_items)
|
||||
self._btn_change_context.setEnabled(has_valid_items)
|
||||
|
||||
else:
|
||||
# Define a custom session so we can query the work root
|
||||
# for a "Work area" that is not our current Session.
|
||||
|
|
@ -291,6 +363,13 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
has_valid_items = self._workarea_files_model.has_valid_items()
|
||||
self._btn_browse.setEnabled(has_valid_items)
|
||||
self._btn_open.setEnabled(has_valid_items)
|
||||
|
||||
if self._publish_context_select_mode:
|
||||
self._btn_select_context_published.setEnabled(
|
||||
bool(self._asset_id) and bool(self._task_name)
|
||||
)
|
||||
return
|
||||
|
||||
# Manually trigger file selection
|
||||
if not has_valid_items:
|
||||
self.on_file_select()
|
||||
|
|
@ -400,11 +479,18 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
session = self._get_session()
|
||||
|
||||
if self.published_enabled:
|
||||
filepath = self._get_selected_filepath()
|
||||
extensions = [os.path.splitext(filepath)[1]]
|
||||
else:
|
||||
extensions = self.host.file_extensions()
|
||||
|
||||
window = SaveAsDialog(
|
||||
parent=self,
|
||||
root=self._workfiles_root,
|
||||
anatomy=self.anatomy,
|
||||
template_key=self.template_key,
|
||||
extensions=extensions,
|
||||
session=session
|
||||
)
|
||||
window.exec_()
|
||||
|
|
@ -462,10 +548,15 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
if work_file:
|
||||
self.open_file(work_file)
|
||||
|
||||
def on_save_as_pressed(self):
|
||||
def _on_save_as_pressed(self):
|
||||
self._save_as_with_dialog()
|
||||
|
||||
def _save_as_with_dialog(self):
|
||||
work_filename = self.get_filename()
|
||||
if not work_filename:
|
||||
return
|
||||
return None
|
||||
|
||||
src_path = self._get_selected_filepath()
|
||||
|
||||
# Trigger before save event
|
||||
emit_event(
|
||||
|
|
@ -486,13 +577,20 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
log.debug("Initializing Work Directory: %s", self._workfiles_root)
|
||||
os.makedirs(self._workfiles_root)
|
||||
|
||||
# Update session if context has changed
|
||||
self._enter_session()
|
||||
# Prepare full path to workfile and save it
|
||||
filepath = os.path.join(
|
||||
os.path.normpath(self._workfiles_root), work_filename
|
||||
)
|
||||
self.host.save_file(filepath)
|
||||
|
||||
# Update session if context has changed
|
||||
self._enter_session()
|
||||
|
||||
if not self.published_enabled:
|
||||
self.host.save_file(filepath)
|
||||
else:
|
||||
shutil.copy(src_path, filepath)
|
||||
self.host.open_file(filepath)
|
||||
|
||||
# Create extra folders
|
||||
create_workdir_extra_folders(
|
||||
self._workdir_path,
|
||||
|
|
@ -510,17 +608,55 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
self.workfile_created.emit(filepath)
|
||||
# Refresh files model
|
||||
self.refresh()
|
||||
if self.published_enabled:
|
||||
self._published_checkbox.setChecked(False)
|
||||
else:
|
||||
self.refresh()
|
||||
return filepath
|
||||
|
||||
def _on_view_published_pressed(self):
|
||||
filepath = self._get_selected_filepath()
|
||||
if not filepath or not os.path.exists(filepath):
|
||||
return
|
||||
item = self._temp_publish_files.add_file(filepath)
|
||||
self.host.open_file(item.filepath)
|
||||
self.publish_file_viewed.emit()
|
||||
# Change state back to workarea
|
||||
self._published_checkbox.setChecked(False)
|
||||
def _on_published_save_as_pressed(self):
|
||||
self._save_as_with_dialog()
|
||||
|
||||
def _set_publish_context_select_mode(self, enabled):
|
||||
self._publish_context_select_mode = enabled
|
||||
|
||||
# Show buttons related to context selection
|
||||
self._publish_context_overlay.setVisible(enabled)
|
||||
self._btn_cancel_published.setVisible(enabled)
|
||||
self._btn_select_context_published.setVisible(enabled)
|
||||
# Change enabled state based on select context
|
||||
self._btn_select_context_published.setEnabled(
|
||||
bool(self._asset_id) and bool(self._task_name)
|
||||
)
|
||||
|
||||
self._btn_save_as_published.setVisible(not enabled)
|
||||
self._btn_change_context.setVisible(not enabled)
|
||||
|
||||
# Change views and disable workarea view if enabled
|
||||
self._workarea_files_view.setEnabled(not enabled)
|
||||
if self.published_enabled:
|
||||
self._workarea_files_view.setVisible(enabled)
|
||||
self._publish_files_view.setVisible(not enabled)
|
||||
else:
|
||||
self._workarea_files_view.setVisible(True)
|
||||
self._publish_files_view.setVisible(False)
|
||||
|
||||
# Disable filter widgets
|
||||
self._published_checkbox.setEnabled(not enabled)
|
||||
self._filter_input.setEnabled(not enabled)
|
||||
|
||||
def _on_publish_change_context_pressed(self):
|
||||
self._set_publish_context_select_mode(True)
|
||||
|
||||
def _on_publish_select_context_pressed(self):
|
||||
result = self._save_as_with_dialog()
|
||||
if result is not None:
|
||||
self._set_publish_context_select_mode(False)
|
||||
self._update_asset_task()
|
||||
|
||||
def _on_publish_cancel_pressed(self):
|
||||
self._set_publish_context_select_mode(False)
|
||||
self._update_asset_task()
|
||||
|
||||
def on_file_select(self):
|
||||
self.file_selected.emit(self._get_selected_filepath())
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import contextlib
|
||||
|
||||
import appdirs
|
||||
|
||||
|
||||
class TempPublishFilesItem(object):
|
||||
"""Object representing copied workfile in app temp folder.
|
||||
|
||||
Args:
|
||||
item_id (str): Id of item used as subfolder.
|
||||
data (dict): Metadata about temp files.
|
||||
directory (str): Path to directory where files are copied to.
|
||||
"""
|
||||
|
||||
def __init__(self, item_id, data, directory):
|
||||
self._id = item_id
|
||||
self._directory = directory
|
||||
self._filepath = os.path.join(directory, data["filename"])
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
return self._directory
|
||||
|
||||
@property
|
||||
def filepath(self):
|
||||
return self._filepath
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
if os.path.exists(self.filepath):
|
||||
s = os.stat(self.filepath)
|
||||
return s.st_size
|
||||
return 0
|
||||
|
||||
|
||||
class TempPublishFiles(object):
|
||||
"""Directory where published workfiles are copied when opened.
|
||||
|
||||
Directory is located in appdirs on the machine. Folder contains file
|
||||
with metadata about stored files. Each item in metadata has id, filename
|
||||
and expiration time. When expiration time is higher then current time the
|
||||
item is removed from metadata and it's files are deleted. Files of items
|
||||
are stored in subfolder named by item's id.
|
||||
|
||||
Metadata file can be in theory opened and modified by multiple processes,
|
||||
threads at one time. For those cases is created simple lock file which
|
||||
is created before modification begins and is removed when modification
|
||||
ends. Existence of the file means that it should not be modified by
|
||||
any other process at the same time.
|
||||
|
||||
Metadata example:
|
||||
```
|
||||
{
|
||||
"96050b4a-8974-4fca-8179-7c446c478d54": {
|
||||
"created": 1647880725.555,
|
||||
"expiration": 1647884325.555,
|
||||
"filename": "cg_pigeon_workfileModeling_v025.ma"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Why is this needed
|
||||
Combination of more issues. Temp files are not automatically removed by
|
||||
OS on windows so using tempfiles in TEMP would lead to kill disk space of
|
||||
machine. There are also cases when someone wants to open multiple files
|
||||
in short period of time and want to manually remove those files so keeping
|
||||
track of temporary copied files in pre-defined structure is needed.
|
||||
"""
|
||||
minute_in_seconds = 60
|
||||
hour_in_seconds = 60 * minute_in_seconds
|
||||
day_in_seconds = 24 * hour_in_seconds
|
||||
|
||||
def __init__(self):
|
||||
root_dir = appdirs.user_data_dir(
|
||||
"published_workfiles_temp", "openpype"
|
||||
)
|
||||
if not os.path.exists(root_dir):
|
||||
os.makedirs(root_dir)
|
||||
|
||||
metadata_path = os.path.join(root_dir, "metadata.json")
|
||||
lock_path = os.path.join(root_dir, "lock.json")
|
||||
|
||||
self._root_dir = root_dir
|
||||
self._metadata_path = metadata_path
|
||||
self._lock_path = lock_path
|
||||
self._log = None
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def life_time(self):
|
||||
"""How long will be new item kept in temp in seconds.
|
||||
|
||||
Returns:
|
||||
int: Lifetime of temp item.
|
||||
"""
|
||||
return int(self.hour_in_seconds)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""File size of existing items."""
|
||||
size = 0
|
||||
for item in self.get_items():
|
||||
size += item.size
|
||||
return size
|
||||
|
||||
def add_file(self, src_path):
|
||||
"""Add workfile to temp directory.
|
||||
|
||||
This will create new item and source path is copied to it's directory.
|
||||
"""
|
||||
filename = os.path.basename(src_path)
|
||||
|
||||
item_id = str(uuid.uuid4())
|
||||
dst_dirpath = os.path.join(self._root_dir, item_id)
|
||||
if not os.path.exists(dst_dirpath):
|
||||
os.makedirs(dst_dirpath)
|
||||
|
||||
dst_path = os.path.join(dst_dirpath, filename)
|
||||
shutil.copy(src_path, dst_path)
|
||||
|
||||
now = time.time()
|
||||
item_data = {
|
||||
"filename": filename,
|
||||
"expiration": now + self.life_time,
|
||||
"created": now
|
||||
}
|
||||
with self._modify_data() as data:
|
||||
data[item_id] = item_data
|
||||
|
||||
return TempPublishFilesItem(item_id, item_data, dst_dirpath)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _modify_data(self):
|
||||
"""Create lock file when data in metadata file are modified."""
|
||||
start_time = time.time()
|
||||
timeout = 3
|
||||
while os.path.exists(self._lock_path):
|
||||
time.sleep(0.01)
|
||||
if start_time > timeout:
|
||||
self.log.warning((
|
||||
"Waited for {} seconds to free lock file. Overriding lock."
|
||||
).format(timeout))
|
||||
|
||||
with open(self._lock_path, "w") as stream:
|
||||
json.dump({"pid": os.getpid()}, stream)
|
||||
|
||||
try:
|
||||
data = self._get_data()
|
||||
yield data
|
||||
with open(self._metadata_path, "w") as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
finally:
|
||||
os.remove(self._lock_path)
|
||||
|
||||
def _get_data(self):
|
||||
output = {}
|
||||
if not os.path.exists(self._metadata_path):
|
||||
return output
|
||||
|
||||
try:
|
||||
with open(self._metadata_path, "r") as stream:
|
||||
output = json.load(stream)
|
||||
except Exception:
|
||||
self.log.warning("Failed to read metadata file.", exc_info=True)
|
||||
return output
|
||||
|
||||
def cleanup(self, check_expiration=True):
|
||||
"""Cleanup files based on metadata.
|
||||
|
||||
Items that passed expiration are removed when this is called. Or all
|
||||
files are removed when `check_expiration` is set to False.
|
||||
|
||||
Args:
|
||||
check_expiration (bool): All items and files are removed when set
|
||||
to True.
|
||||
"""
|
||||
data = self._get_data()
|
||||
now = time.time()
|
||||
remove_ids = set()
|
||||
all_ids = set()
|
||||
for item_id, item_data in data.items():
|
||||
all_ids.add(item_id)
|
||||
if check_expiration and now < item_data["expiration"]:
|
||||
continue
|
||||
|
||||
remove_ids.add(item_id)
|
||||
|
||||
for item_id in remove_ids:
|
||||
try:
|
||||
self.remove_id(item_id)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to remove temp publish item \"{}\"".format(
|
||||
item_id
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Remove unknown folders/files
|
||||
for filename in os.listdir(self._root_dir):
|
||||
if filename in all_ids:
|
||||
continue
|
||||
|
||||
full_path = os.path.join(self._root_dir, filename)
|
||||
if full_path in (self._metadata_path, self._lock_path):
|
||||
continue
|
||||
|
||||
try:
|
||||
shutil.rmtree(full_path)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Couldn't remove arbitrary path \"{}\"".format(full_path),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.cleanup(False)
|
||||
|
||||
def get_items(self):
|
||||
"""Receive all items from metadata file.
|
||||
|
||||
Returns:
|
||||
list<TempPublishFilesItem>: Info about each item in metadata.
|
||||
"""
|
||||
output = []
|
||||
data = self._get_data()
|
||||
for item_id, item_data in data.items():
|
||||
item_path = os.path.join(self._root_dir, item_id)
|
||||
output.append(TempPublishFilesItem(item_id, item_data, item_path))
|
||||
return output
|
||||
|
||||
def remove_id(self, item_id):
|
||||
"""Remove files of item and then remove the item from metadata."""
|
||||
filepath = os.path.join(self._root_dir, item_id)
|
||||
if os.path.exists(filepath):
|
||||
shutil.rmtree(filepath)
|
||||
|
||||
with self._modify_data() as data:
|
||||
data.pop(item_id, None)
|
||||
|
||||
|
||||
def file_size_to_string(file_size):
|
||||
size = 0
|
||||
size_ending_mapping = {
|
||||
"KB": 1024 ** 1,
|
||||
"MB": 1024 ** 2,
|
||||
"GB": 1024 ** 3
|
||||
}
|
||||
ending = "B"
|
||||
for _ending, _size in size_ending_mapping.items():
|
||||
if file_size < _size:
|
||||
break
|
||||
size = file_size / _size
|
||||
ending = _ending
|
||||
return "{:.2f} {}".format(size, ending)
|
||||
|
|
@ -193,7 +193,9 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, parent, root, anatomy, template_key, session=None):
|
||||
def __init__(
|
||||
self, parent, root, anatomy, template_key, extensions, session=None
|
||||
):
|
||||
super(SaveAsDialog, self).__init__(parent=parent)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
|
||||
|
|
@ -201,6 +203,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
self.host = api.registered_host()
|
||||
self.root = root
|
||||
self.work_file = None
|
||||
self._extensions = extensions
|
||||
|
||||
if not session:
|
||||
# Fallback to active session
|
||||
|
|
@ -257,7 +260,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
# Add styled delegate to use stylesheets
|
||||
ext_delegate = QtWidgets.QStyledItemDelegate()
|
||||
ext_combo.setItemDelegate(ext_delegate)
|
||||
ext_combo.addItems(self.host.file_extensions())
|
||||
ext_combo.addItems(self._extensions)
|
||||
|
||||
# Build inputs
|
||||
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
|
||||
|
|
@ -336,7 +339,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
def get_existing_comments(self):
|
||||
matcher = CommentMatcher(self.anatomy, self.template_key, self.data)
|
||||
host_extensions = set(self.host.file_extensions())
|
||||
host_extensions = set(self._extensions)
|
||||
comments = set()
|
||||
if os.path.isdir(self.root):
|
||||
for fname in os.listdir(self.root):
|
||||
|
|
@ -392,7 +395,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
return anatomy_filled[self.template_key]["file"]
|
||||
|
||||
def refresh(self):
|
||||
extensions = self.host.file_extensions()
|
||||
extensions = list(self._extensions)
|
||||
extension = self.data["ext"]
|
||||
if extension is None:
|
||||
# Define saving file extension
|
||||
|
|
|
|||
|
|
@ -14,7 +14,22 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
|
|||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
|
||||
from .files_widget import FilesWidget
|
||||
from .lib import TempPublishFiles, file_size_to_string
|
||||
|
||||
|
||||
def file_size_to_string(file_size):
|
||||
size = 0
|
||||
size_ending_mapping = {
|
||||
"KB": 1024 ** 1,
|
||||
"MB": 1024 ** 2,
|
||||
"GB": 1024 ** 3
|
||||
}
|
||||
ending = "B"
|
||||
for _ending, _size in size_ending_mapping.items():
|
||||
if file_size < _size:
|
||||
break
|
||||
size = file_size / _size
|
||||
ending = _ending
|
||||
return "{:.2f} {}".format(size, ending)
|
||||
|
||||
|
||||
class SidePanelWidget(QtWidgets.QWidget):
|
||||
|
|
@ -44,67 +59,25 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
btn_note_save, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
publish_temp_widget = QtWidgets.QWidget(self)
|
||||
publish_temp_info_label = QtWidgets.QLabel(
|
||||
self.published_workfile_message.format(
|
||||
file_size_to_string(0)
|
||||
),
|
||||
publish_temp_widget
|
||||
)
|
||||
publish_temp_info_label.setWordWrap(True)
|
||||
|
||||
btn_clear_temp = QtWidgets.QPushButton(
|
||||
"Clear temp", publish_temp_widget
|
||||
)
|
||||
|
||||
publish_temp_layout = QtWidgets.QVBoxLayout(publish_temp_widget)
|
||||
publish_temp_layout.setContentsMargins(0, 0, 0, 0)
|
||||
publish_temp_layout.addWidget(publish_temp_info_label, 0)
|
||||
publish_temp_layout.addWidget(
|
||||
btn_clear_temp, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(details_label, 0)
|
||||
main_layout.addWidget(details_input, 1)
|
||||
main_layout.addWidget(artist_note_widget, 1)
|
||||
main_layout.addWidget(publish_temp_widget, 0)
|
||||
|
||||
note_input.textChanged.connect(self._on_note_change)
|
||||
btn_note_save.clicked.connect(self._on_save_click)
|
||||
btn_clear_temp.clicked.connect(self._on_clear_temp_click)
|
||||
|
||||
self._details_input = details_input
|
||||
self._artist_note_widget = artist_note_widget
|
||||
self._note_input = note_input
|
||||
self._btn_note_save = btn_note_save
|
||||
|
||||
self._publish_temp_info_label = publish_temp_info_label
|
||||
self._publish_temp_widget = publish_temp_widget
|
||||
|
||||
self._orig_note = ""
|
||||
self._workfile_doc = None
|
||||
|
||||
publish_temp_widget.setVisible(False)
|
||||
|
||||
def set_published_visible(self, published_visible):
|
||||
self._artist_note_widget.setVisible(not published_visible)
|
||||
self._publish_temp_widget.setVisible(published_visible)
|
||||
if published_visible:
|
||||
self.refresh_publish_temp_sizes()
|
||||
|
||||
def refresh_publish_temp_sizes(self):
|
||||
temp_publish_files = TempPublishFiles()
|
||||
text = self.published_workfile_message.format(
|
||||
file_size_to_string(temp_publish_files.size)
|
||||
)
|
||||
self._publish_temp_info_label.setText(text)
|
||||
|
||||
def _on_clear_temp_click(self):
|
||||
temp_publish_files = TempPublishFiles()
|
||||
temp_publish_files.clear()
|
||||
self.refresh_publish_temp_sizes()
|
||||
|
||||
def _on_note_change(self):
|
||||
text = self._note_input.toPlainText()
|
||||
|
|
@ -225,9 +198,6 @@ class Window(QtWidgets.QMainWindow):
|
|||
files_widget.file_selected.connect(self.on_file_select)
|
||||
files_widget.workfile_created.connect(self.on_workfile_create)
|
||||
files_widget.file_opened.connect(self._on_file_opened)
|
||||
files_widget.publish_file_viewed.connect(
|
||||
self._on_publish_file_viewed
|
||||
)
|
||||
files_widget.published_visible_changed.connect(
|
||||
self._on_published_change
|
||||
)
|
||||
|
|
@ -292,9 +262,6 @@ class Window(QtWidgets.QMainWindow):
|
|||
def _on_file_opened(self):
|
||||
self.close()
|
||||
|
||||
def _on_publish_file_viewed(self):
|
||||
self.side_panel.refresh_publish_temp_sizes()
|
||||
|
||||
def _on_published_change(self, visible):
|
||||
self.side_panel.set_published_visible(visible)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.9.2-nightly.3"
|
||||
__version__ = "3.9.3-nightly.1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue