[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-01-24 20:57:10 +01:00 committed by GitHub
commit 51762f94cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 559 additions and 164 deletions

View file

@ -280,7 +280,7 @@ def shape_from_element(element):
return node
def collect_animation_data():
def collect_animation_data(fps=False):
"""Get the basic animation data
Returns:
@ -291,7 +291,6 @@ def collect_animation_data():
# get scene values as defaults
start = cmds.playbackOptions(query=True, animationStartTime=True)
end = cmds.playbackOptions(query=True, animationEndTime=True)
fps = mel.eval('currentTimeUnitToFPS()')
# build attributes
data = OrderedDict()
@ -299,7 +298,9 @@ def collect_animation_data():
data["frameEnd"] = end
data["handles"] = 0
data["step"] = 1.0
data["fps"] = fps
if fps:
data["fps"] = mel.eval('currentTimeUnitToFPS()')
return data
@ -2853,3 +2854,27 @@ def set_colorspace():
cmds.colorManagementPrefs(e=True, renderingSpaceName=renderSpace)
viewTransform = root_dict["viewTransform"]
cmds.colorManagementPrefs(e=True, viewTransformName=viewTransform)
@contextlib.contextmanager
def root_parent(nodes):
# type: (list) -> list
"""Context manager to un-parent provided nodes and return then back."""
import pymel.core as pm # noqa
node_parents = []
for node in nodes:
n = pm.PyNode(node)
try:
root = pm.listRelatives(n, parent=1)[0]
except IndexError:
root = None
node_parents.append((n, root))
try:
for node in node_parents:
node[0].setParent(world=True)
yield
finally:
for node in node_parents:
if node[1]:
node[0].setParent(node[1])

View file

@ -22,7 +22,7 @@ class CreateReview(plugin.Creator):
# get basic animation data : start / end / handles / steps
data = OrderedDict(**self.data)
animation_data = lib.collect_animation_data()
animation_data = lib.collect_animation_data(fps=True)
for key, value in animation_data.items():
data[key] = value

View file

@ -1,11 +1,58 @@
from openpype.hosts.maya.api import plugin
# -*- coding: utf-8 -*-
"""Creator for Unreal Static Meshes."""
from openpype.hosts.maya.api import plugin, lib
from avalon.api import Session
from openpype.api import get_project_settings
from maya import cmds # noqa
class CreateUnrealStaticMesh(plugin.Creator):
"""Unreal Static Meshes with collisions."""
name = "staticMeshMain"
label = "Unreal - Static Mesh"
family = "unrealStaticMesh"
icon = "cube"
dynamic_subset_keys = ["asset"]
def __init__(self, *args, **kwargs):
"""Constructor."""
super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs)
self._project_settings = get_project_settings(
Session["AVALON_PROJECT"])
@classmethod
def get_dynamic_data(
cls, variant, task_name, asset_id, project_name, host_name
):
dynamic_data = super(CreateUnrealStaticMesh, 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):
with lib.undo_chunk():
instance = super(CreateUnrealStaticMesh, 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)
collisions_set = cmds.sets(name="collisions_SET", empty=True)
cmds.sets([geometry_set, collisions_set], forceElement=instance)
members = cmds.ls(content, long=True) or []
children = cmds.listRelatives(members, allDescendents=True,
fullPath=True) or []
children = cmds.ls(children, type="transform")
for node in children:
if cmds.listRelatives(node, type="shape"):
if [
n for n in self.collision_prefixes
if node.startswith(n)
]:
cmds.sets(node, forceElement=collisions_set)
else:
cmds.sets(node, forceElement=geometry_set)

View file

@ -17,8 +17,8 @@ from openpype.api import get_project_settings
class VRayProxyLoader(api.Loader):
"""Load VRay Proxy with Alembic or VrayMesh."""
families = ["vrayproxy"]
representations = ["vrmesh"]
families = ["vrayproxy", "model", "pointcache", "animation"]
representations = ["vrmesh", "abc"]
label = "Import VRay Proxy"
order = -10

View file

@ -0,0 +1,31 @@
# -*- 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

View file

@ -4,25 +4,31 @@ import pyblish.api
class CollectUnrealStaticMesh(pyblish.api.InstancePlugin):
"""Collect unreal static mesh
"""Collect Unreal Static Mesh
Ensures always only a single frame is extracted (current frame). This
also sets correct FBX options for later extraction.
Note:
This is a workaround so that the `pype.model` family can use the
same pointcache extractor implementation as animation and pointcaches.
This always enforces the "current" frame to be published.
"""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Model Data"
label = "Collect Unreal Static Meshes"
families = ["unrealStaticMesh"]
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, query=True)
collision_set = [i for i in instance if i == "collisions_SET"]
instance.data["collisionMembers"] = cmds.sets(
collision_set, query=True)
# set fbx overrides on instance
instance.data["smoothingGroups"] = True
instance.data["smoothMesh"] = True

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import os
from maya import cmds
import maya.mel as mel
from maya import cmds # noqa
import maya.mel as mel # noqa
from openpype.hosts.maya.api.lib import root_parent
import pyblish.api
import avalon.maya
@ -192,10 +194,7 @@ class ExtractFBX(openpype.api.Extractor):
if isinstance(value, bool):
value = str(value).lower()
template = "FBXExport{0} -v {1}"
if key == "UpAxis":
template = "FBXExport{0} {1}"
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)
@ -205,9 +204,16 @@ class ExtractFBX(openpype.api.Extractor):
mel.eval("FBXExportGenerateLog -v false")
# Export
with avalon.maya.maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
if "unrealStaticMesh" in instance.data["families"]:
with avalon.maya.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 avalon.maya.maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
if "representations" not in instance.data:
instance.data["representations"] = []

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Create Unreal Static Mesh data to be extracted as FBX."""
import openpype.api
import pyblish.api
from maya import cmds # noqa
class ExtractUnrealStaticMesh(openpype.api.Extractor):
"""Extract FBX from Maya. """
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Unreal Static Mesh"
families = ["unrealStaticMesh"]
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)
if not instance.data.get("cleanNodes"):
instance.data["cleanNodes"] = []
instance.data["cleanNodes"].append(static_mesh_name)
instance.data["cleanNodes"] += duplicates
instance.data["setMembers"] = [static_mesh_name]
instance.data["setMembers"] += instance.data["collisionMembers"]

View file

@ -30,7 +30,8 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin):
descendants = cmds.listRelatives(content_instance,
allDescendents=True,
fullPath=True) or []
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
descendants = cmds.ls(
descendants, noIntermediate=True, type="transform")
content_instance = list(set(content_instance + descendants))
assemblies = cmds.ls(content_instance, assemblies=True, long=True)

View file

@ -1,27 +1,30 @@
# -*- coding: utf-8 -*-
from maya import cmds
from maya import cmds # noqa
import pyblish.api
import openpype.api
import openpype.hosts.maya.api.action
from avalon.api import Session
from openpype.api import get_project_settings
import re
class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
"""Validate name of Unreal Static Mesh
Unreals naming convention states that staticMesh should start with `SM`
prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other
types of meshes - collision meshes:
prefix - SM_[Name]_## (Eg. SM_sube_01).These prefixes can be configured
in Settings UI. This plugin also validates other types of
meshes - collision meshes:
UBX_[RenderMeshName]_##:
UBX_[RenderMeshName]*:
Boxes are created with the Box objects type in
Max or with the Cube polygonal primitive in Maya.
You cannot move the vertices around or deform it
in any way to make it something other than a
rectangular prism, or else it will not work.
UCP_[RenderMeshName]_##:
UCP_[RenderMeshName]*:
Capsules are created with the Capsule object type.
The capsule does not need to have many segments
(8 is a good number) at all because it is
@ -29,7 +32,7 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
boxes, you should not move the individual
vertices around.
USP_[RenderMeshName]_##:
USP_[RenderMeshName]*:
Spheres are created with the Sphere object type.
The sphere does not need to have many segments
(8 is a good number) at all because it is
@ -37,7 +40,7 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
boxes, you should not move the individual
vertices around.
UCX_[RenderMeshName]_##:
UCX_[RenderMeshName]*:
Convex objects can be any completely closed
convex 3D shape. For example, a box can also be
a convex object
@ -52,67 +55,86 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
families = ["unrealStaticMesh"]
label = "Unreal StaticMesh Name"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
regex_mesh = r"SM_(?P<renderName>.*)_(\d{2})"
regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P<renderName>.*)_(\d{2})"
regex_mesh = r"(?P<renderName>.*))"
regex_collision = r"(?P<renderName>.*)"
@classmethod
def get_invalid(cls, instance):
# find out if supplied transform is group or not
def is_group(groupName):
try:
children = cmds.listRelatives(groupName, children=True)
for child in children:
if not cmds.ls(child, transforms=True):
return False
invalid = []
project_settings = get_project_settings(Session["AVALON_PROJECT"])
collision_prefixes = (
project_settings
["maya"]
["create"]
["CreateUnrealStaticMesh"]
["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):
cls.log.error("Mesh doesn't comply with name validation.")
return True
except Exception:
if cls.validate_collision:
collision_set = instance.data.get("collisionMembers", None)
# soft-fail is there are no collision objects
if not collision_set:
cls.log.warning("No collision objects to validate.")
return False
invalid = []
content_instance = instance.data.get("setMembers", None)
if not content_instance:
cls.log.error("Instance has no nodes!")
return True
pass
descendants = cmds.listRelatives(content_instance,
allDescendents=True,
fullPath=True) or []
regex_collision = "{}{}".format(
"(?P<prefix>({}))_".format(
"|".join("{0}".format(p) for p in collision_prefixes)
) or "", cls.regex_collision
)
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
trns = cmds.ls(descendants, long=False, type=('transform'))
cl_r = re.compile(regex_collision)
# filter out groups
filter = [node for node in trns if not is_group(node)]
# compile regex for testing names
sm_r = re.compile(cls.regex_mesh)
cl_r = re.compile(cls.regex_collision)
sm_names = []
col_names = []
for obj in filter:
sm_m = sm_r.match(obj)
if sm_m is None:
# test if it matches collision mesh
cl_r = sm_r.match(obj)
if cl_r is None:
cls.log.error("invalid mesh name on: {}".format(obj))
for obj in collision_set:
cl_m = cl_r.match(obj)
if not cl_m:
cls.log.error("{} is invalid".format(obj))
invalid.append(obj)
else:
col_names.append((cl_r.group("renderName"), obj))
else:
sm_names.append(sm_m.group("renderName"))
expected_collision = "{}_{}".format(
cl_m.group("prefix"),
combined_geometry_name
)
for c_mesh in col_names:
if c_mesh[0] not in sm_names:
cls.log.error(("collision name {} doesn't match any "
"static mesh names.").format(obj))
invalid.append(c_mesh[1])
if not obj.startswith(expected_collision):
cls.log.error(
"Collision object name doesn't match "
"static mesh name"
)
cls.log.error("{}_{} != {}_{}".format(
cl_m.group("prefix"),
cl_m.group("renderName"),
cl_m.group("prefix"),
combined_geometry_name,
))
invalid.append(obj)
return invalid
def process(self, instance):
if not self.validate_mesh and not self.validate_collision:
self.log.info("Validation of both mesh and collision names"
"is disabled.")
return
if not instance.data.get("collisionMembers", None):
self.log.info("There are no collision objects to validate")
return
invalid = self.get_invalid(instance)

View file

@ -175,7 +175,8 @@ from .openpype_version import (
get_expected_version,
is_running_from_build,
is_running_staging,
is_current_version_studio_latest
is_current_version_studio_latest,
is_current_version_higher_than_expected
)
terminal = Terminal

View file

@ -195,3 +195,32 @@ def is_current_version_studio_latest():
expected_version = get_expected_version()
# Check if current version is expected version
return current_version == expected_version
def is_current_version_higher_than_expected():
"""Is current OpenPype version higher than version defined by studio.
Returns:
None: Can't determine. e.g. when running from code or the build is
too old.
bool: True when is higher than studio version.
"""
output = None
# Skip if is not running from build or build does not support version
# control or path to folder with zip files is not accessible
if (
not is_running_from_build()
or not op_version_control_available()
or not openpype_path_is_accessible()
):
return output
# Get OpenPypeVersion class
OpenPypeVersion = get_OpenPypeVersion()
# Convert current version to OpenPypeVersion object
current_version = OpenPypeVersion(version=get_openpype_version())
# Get expected version (from settings)
expected_version = get_expected_version()
# Check if current version is expected version
return current_version > expected_version

View file

@ -10,11 +10,12 @@ from .execute import get_openpype_execute_args
from .local_settings import get_local_site_id
from .openpype_version import (
is_running_from_build,
get_openpype_version
get_openpype_version,
get_build_version
)
def get_pype_info():
def get_openpype_info():
"""Information about currently used Pype process."""
executable_args = get_openpype_execute_args()
if is_running_from_build():
@ -23,6 +24,7 @@ def get_pype_info():
version_type = "code"
return {
"build_verison": get_build_version(),
"version": get_openpype_version(),
"version_type": version_type,
"executable": executable_args[-1],
@ -51,7 +53,7 @@ def get_workstation_info():
def get_all_current_info():
"""All information about current process in one dictionary."""
return {
"pype": get_pype_info(),
"pype": get_openpype_info(),
"workstation": get_workstation_info(),
"env": os.environ.copy(),
"local_settings": get_local_settings()

View file

@ -389,6 +389,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
repre["ext"] = ext
template_data["ext"] = ext
self.log.info(template_name)
template = os.path.normpath(
anatomy.templates[template_name]["path"])

View file

@ -27,5 +27,10 @@
"path": "{@folder}/{@file}"
},
"delivery": {},
"unreal": {
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}",
"file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
"path": "{@folder}/{@file}"
},
"others": {}
}

View file

@ -219,7 +219,7 @@
"hosts": [],
"task_types": [],
"tasks": [],
"template": "{family}{Variant}"
"template": "{family}{variant}"
},
{
"families": [
@ -264,6 +264,17 @@
"task_types": [],
"tasks": [],
"template": "render{Task}{Variant}"
},
{
"families": [
"unrealStaticMesh"
],
"hosts": [
"maya"
],
"task_types": [],
"tasks": [],
"template": "S_{asset}{variant}"
}
]
},

View file

@ -46,6 +46,20 @@
"aov_separator": "underscore",
"default_render_image_folder": "renders"
},
"CreateUnrealStaticMesh": {
"enabled": true,
"defaults": [
"",
"_Main"
],
"static_mesh_prefix": "S_",
"collision_prefixes": [
"UBX",
"UCP",
"USP",
"UCX"
]
},
"CreateAnimation": {
"enabled": true,
"defaults": [
@ -123,12 +137,6 @@
"Anim"
]
},
"CreateUnrealStaticMesh": {
"enabled": true,
"defaults": [
"Main"
]
},
"CreateVrayProxy": {
"enabled": true,
"defaults": [
@ -180,6 +188,11 @@
"whitelist_native_plugins": false,
"authorized_plugins": []
},
"ValidateUnrealStaticMeshName": {
"enabled": true,
"validate_mesh": false,
"validate_collision": true
},
"ValidateRenderSettings": {
"arnold_render_attributes": [],
"vray_render_attributes": [],
@ -197,6 +210,11 @@
"regex": "(.*)_(\\d)*_(?P<shader>.*)_(GEO)",
"top_level_regex": ".*_GRP"
},
"ValidateModelContent": {
"enabled": true,
"optional": false,
"validate_top_group": true
},
"ValidateTransformNamingSuffix": {
"enabled": true,
"SUFFIX_NAMING_TABLE": {
@ -281,11 +299,6 @@
"optional": true,
"active": true
},
"ValidateModelContent": {
"enabled": true,
"optional": false,
"validate_top_group": true
},
"ValidateNoAnimation": {
"enabled": false,
"optional": true,

View file

@ -2,9 +2,6 @@
"studio_name": "Studio name",
"studio_code": "stu",
"admin_password": "",
"production_version": "",
"staging_version": "",
"version_check_interval": 5,
"environment": {
"__environment_keys__": {
"global": []
@ -19,5 +16,8 @@
"windows": [],
"darwin": [],
"linux": []
}
},
"production_version": "",
"staging_version": "",
"version_check_interval": 5
}

View file

@ -143,6 +143,28 @@
"label": "Delivery",
"object_type": "text"
},
{
"type": "dict",
"key": "unreal",
"label": "Unreal",
"children": [
{
"type": "text",
"key": "folder",
"label": "Folder"
},
{
"type": "text",
"key": "file",
"label": "File"
},
{
"type": "text",
"key": "path",
"label": "Path"
}
]
},
{
"type": "dict-modifiable",
"key": "others",

View file

@ -66,6 +66,38 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CreateUnrealStaticMesh",
"label": "Create Unreal - Static Mesh",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"object_type": "text"
},
{
"type": "text",
"key": "static_mesh_prefix",
"label": "Static Mesh Prefix"
},
{
"type": "list",
"key": "collision_prefixes",
"label": "Collision Mesh Prefixes",
"object_type": "text"
}
]
},
{
"type": "schema_template",
"name": "template_create_plugin",
@ -118,10 +150,6 @@
"key": "CreateSetDress",
"label": "Create Set Dress"
},
{
"key": "CreateUnrealStaticMesh",
"label": "Create Unreal - Static Mesh"
},
{
"key": "CreateVrayProxy",
"label": "Create VRay Proxy"

View file

@ -129,6 +129,31 @@
]
},
{
"type": "dict",
"collapsible": true,
"key": "ValidateUnrealStaticMeshName",
"label": "Validate Unreal Static Mesh Name",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "validate_mesh",
"label": "Validate mesh Names "
},
{
"type": "boolean",
"key": "validate_collision",
"label": "Validate collision names"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -30,36 +30,6 @@
{
"type": "splitter"
},
{
"type": "label",
"label": "Define explicit OpenPype version that should be used. Keep empty to use latest available version."
},
{
"type": "production-versions-text",
"key": "production_version",
"label": "Production version"
},
{
"type": "staging-versions-text",
"key": "staging_version",
"label": "Staging version"
},
{
"type": "splitter"
},
{
"type": "label",
"label": "Trigger validation if running OpenPype is using studio defined version each 'n' <b>minutes</b>. Validation happens in OpenPype tray application."
},
{
"type": "number",
"key": "version_check_interval",
"label": "Version check interval",
"minimum": 0
},
{
"type": "splitter"
},
{
"key": "environment",
"label": "Environment",
@ -141,12 +111,49 @@
"type": "splitter"
},
{
"type": "path",
"key": "openpype_path",
"label": "Versions Repository",
"multiplatform": true,
"multipath": true,
"require_restart": true
"type": "collapsible-wrap",
"label": "OpenPype deployment control",
"collapsible": false,
"children": [
{
"type": "path",
"key": "openpype_path",
"label": "Versions Repository",
"multiplatform": true,
"multipath": true,
"require_restart": true
},
{
"type": "splitter"
},
{
"type": "label",
"label": "Define explicit OpenPype version that should be used. Keep empty to use latest available version."
},
{
"type": "production-versions-text",
"key": "production_version",
"label": "Production version"
},
{
"type": "staging-versions-text",
"key": "staging_version",
"label": "Staging version"
},
{
"type": "splitter"
},
{
"type": "label",
"label": "Trigger validation if running OpenPype is using studio defined version each 'n' <b>minutes</b>. Validation happens in OpenPype tray application."
},
{
"type": "number",
"key": "version_check_interval",
"label": "Version check interval",
"minimum": 0
}
]
}
]
}

View file

@ -92,8 +92,7 @@ class CollapsibleWrapper(WrapperWidget):
self.content_layout = content_layout
if self.collapsible:
if not self.collapsed:
body_widget.toggle_content()
body_widget.toggle_content(self.collapsed)
else:
body_widget.hide_toolbox(hide_content=False)

View file

@ -9,7 +9,7 @@ from openpype.api import resources
from openpype.settings.lib import get_local_settings
from openpype.lib.pype_info import (
get_all_current_info,
get_pype_info,
get_openpype_info,
get_workstation_info,
extract_pype_info_to_file
)
@ -426,7 +426,7 @@ class PypeInfoSubWidget(QtWidgets.QWidget):
"""Create widget with information about OpenPype application."""
# Get pype info data
pype_info = get_pype_info()
pype_info = get_openpype_info()
# Modify version key/values
version_value = "{} ({})".format(
pype_info.pop("version", self.not_applicable),
@ -435,13 +435,20 @@ class PypeInfoSubWidget(QtWidgets.QWidget):
pype_info["version_value"] = version_value
# Prepare lable mapping
key_label_mapping = {
"version_value": "OpenPype version:",
"version_value": "Running version:",
"build_verison": "Build version:",
"executable": "OpenPype executable:",
"pype_root": "OpenPype location:",
"mongo_url": "OpenPype Mongo URL:"
}
# Prepare keys order
keys_order = ["version_value", "executable", "pype_root", "mongo_url"]
keys_order = [
"version_value",
"build_verison",
"executable",
"pype_root",
"mongo_url"
]
for key in pype_info.keys():
if key not in keys_order:
keys_order.append(key)

View file

@ -18,6 +18,7 @@ from openpype.lib import (
get_openpype_execute_args,
op_version_control_available,
is_current_version_studio_latest,
is_current_version_higher_than_expected,
is_running_from_build,
is_running_staging,
get_expected_version,
@ -84,7 +85,7 @@ class VersionDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(VersionDialog, self).__init__(parent)
self.setWindowTitle("OpenPype update is needed")
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
@ -104,13 +105,12 @@ class VersionDialog(QtWidgets.QDialog):
label_widget.setWordWrap(True)
top_layout = QtWidgets.QHBoxLayout(top_widget)
# top_layout.setContentsMargins(0, 0, 0, 0)
top_layout.setSpacing(10)
top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter)
top_layout.addWidget(label_widget, 1)
ignore_btn = QtWidgets.QPushButton("Later", self)
restart_btn = QtWidgets.QPushButton("Restart && Update", self)
ignore_btn = QtWidgets.QPushButton(self)
restart_btn = QtWidgets.QPushButton(self)
restart_btn.setObjectName("TrayRestartButton")
btns_layout = QtWidgets.QHBoxLayout()
@ -127,7 +127,12 @@ class VersionDialog(QtWidgets.QDialog):
restart_btn.clicked.connect(self._on_reset)
self._label_widget = label_widget
self._gift_icon_label = gift_icon_label
self._ignore_btn = ignore_btn
self._restart_btn = restart_btn
self._restart_accepted = False
self._current_is_higher = False
self.setStyleSheet(style.load_stylesheet())
@ -152,15 +157,41 @@ class VersionDialog(QtWidgets.QDialog):
def closeEvent(self, event):
super().closeEvent(event)
if not self._restart_accepted:
self.ignore_requested.emit()
if self._restart_accepted or self._current_is_higher:
return
# Trigger ignore requested only if restart was not clicked and current
# version is lower
self.ignore_requested.emit()
def update_versions(self, current_version, expected_version):
message = (
"Running OpenPype version is <b>{}</b>."
" Your production has been updated to version <b>{}</b>."
).format(str(current_version), str(expected_version))
self._label_widget.setText(message)
def update_versions(
self, current_version, expected_version, current_is_higher
):
if not current_is_higher:
title = "OpenPype update is needed"
label_message = (
"Running OpenPype version is <b>{}</b>."
" Your production has been updated to version <b>{}</b>."
).format(str(current_version), str(expected_version))
ignore_label = "Later"
restart_label = "Restart && Update"
else:
title = "OpenPype version is higher"
label_message = (
"Running OpenPype version is <b>{}</b>."
" Your production uses version <b>{}</b>."
).format(str(current_version), str(expected_version))
ignore_label = "Ignore"
restart_label = "Restart && Change"
self.setWindowTitle(title)
self._current_is_higher = current_is_higher
self._gift_icon_label.setVisible(not current_is_higher)
self._label_widget.setText(label_message)
self._ignore_btn.setText(ignore_label)
self._restart_btn.setText(restart_label)
def _on_ignore(self):
self.reject()
@ -227,6 +258,10 @@ class TrayManager:
def validate_openpype_version(self):
using_requested = is_current_version_studio_latest()
# TODO Handle situations when version can't be detected
if using_requested is None:
using_requested = True
self._restart_action.setVisible(not using_requested)
if using_requested:
if (
@ -247,15 +282,17 @@ class TrayManager:
expected_version = get_expected_version()
current_version = get_openpype_version()
current_is_higher = is_current_version_higher_than_expected()
self._version_dialog.update_versions(
current_version, expected_version
current_version, expected_version, current_is_higher
)
self._version_dialog.show()
self._version_dialog.raise_()
self._version_dialog.activateWindow()
def _restart_and_install(self):
self.restart()
self.restart(use_expected_version=True)
def _outdated_version_ignored(self):
self.show_tray_message(
@ -328,8 +365,8 @@ class TrayManager:
self.main_thread_timer = main_thread_timer
version_check_timer = QtCore.QTimer()
version_check_timer.timeout.connect(self._on_version_check_timer)
if self._version_check_interval > 0:
version_check_timer.timeout.connect(self._on_version_check_timer)
version_check_timer.setInterval(self._version_check_interval)
version_check_timer.start()
self._version_check_timer = version_check_timer
@ -341,6 +378,9 @@ class TrayManager:
def _startup_validations(self):
"""Run possible startup validations."""
# Trigger version validation on start
self._version_check_timer.timeout.emit()
self._validate_settings_defaults()
def _validate_settings_defaults(self):
@ -429,12 +469,18 @@ class TrayManager:
self._restart_action = restart_action
def _on_restart_action(self):
self.restart()
self.restart(use_expected_version=True)
def restart(self, reset_version=True):
def restart(self, use_expected_version=False, reset_version=False):
"""Restart Tray tool.
First creates new process with same argument and close current tray.
Args:
use_expected_version(bool): OpenPype version is set to expected
version.
reset_version(bool): OpenPype version is cleaned up so igniters
logic will decide which version will be used.
"""
args = get_openpype_execute_args()
kwargs = {
@ -448,6 +494,15 @@ class TrayManager:
if args[-1] == additional_args[0]:
additional_args.pop(0)
if use_expected_version:
expected_version = get_expected_version()
if expected_version is not None:
reset_version = False
kwargs["env"]["OPENPYPE_VERSION"] = str(expected_version)
else:
# Trigger reset of version if expected version was not found
reset_version = True
# Pop OPENPYPE_VERSION
if reset_version:
# Add staging flag if was running from staging

@ -1 +1 @@
Subproject commit ffe9e910f1f382e222d457d8e4a8426c41ed43ae
Subproject commit 159d2f23e4c79c04dfac57b68d2ee6ac67adec1b

View file

@ -130,8 +130,8 @@ main () {
fi
echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..."
PYTHONPATH="$openpype_root:$PYTHONPATH"
OPENPYPE_ROOT="$openpype_root"
export PYTHONPATH="$openpype_root:$PYTHONPATH"
export OPENPYPE_ROOT="$openpype_root"
"$POETRY_HOME/bin/poetry" run python3 "$openpype_root/tools/create_zip.py" "$@"
}

View file

@ -11,6 +11,8 @@ import TabItem from '@theme/TabItem';
Settings applicable to the full studio.
![general_settings](assets/settings/settings_system_general.png)
**`Studio Name`** - Full name of the studio (can be used as variable on some places)
**`Studio Code`** - Studio acronym or a short code (can be used as variable on some places)
@ -24,10 +26,27 @@ as a naive barier to prevent artists from accidental setting changes.
**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up.
Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume).
### OpenPype deployment control
**`Versions Repository`** - Location where automatic update mechanism searches for zip files with
OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute#2-openpype-codebase)
![general_settings](assets/settings/settings_system_general.png)
**`Production version`** - Define what is current production version. When value is not set then latest version available in versions repository is resolved as production version.
**`Staging version`** - Define what is current staging version. When value is not set then latest staging version available in versions repository is resolved as staging version.
For more information about Production and Staging go to [Distribute](admin_distribute#staging-vs-production).
**Production version** and **Staging version** fields will define which version will be used in studio. Filling explicit version will force new OpenPype processes to use it. That gives more control over studio deployment especially when some workstations don't have access to version repository (e.g. remote users). It can be also used to downgrade studio version when newer version have production breaking bug.
When fields are not filled the latest version in versions repository is used as studio version. That makes updating easier as it is not needed to modify settings but workstations without access to versions repository can't find out which OpenPype version should be used.
If version repository is not set or is not accessible for workstation the latest available version on workstation is used or version inside build.
**`Version check interval`** - OpenPype tray application check if currently used OpenPype version is up to date with production/staging version. It is possible to modify how often the validation is triggered in minutes. It is possible to set the interval to `0`. That will turn off version validations but it is not recommend.
A dialog asking for restart is shown when OpenPype tray application detect that different version should be used.
![general_settings](assets/settings/settings_system_version_update.png)
![general_settings](assets/settings/settings_system_version_downgrade.png)
## Modules

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB