mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
resolved conflict
This commit is contained in:
commit
fe1fd463da
37 changed files with 2459 additions and 210 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.17.4-nightly.1
|
||||
- 3.17.3
|
||||
- 3.17.3-nightly.2
|
||||
- 3.17.3-nightly.1
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.15.0
|
||||
- 3.15.0-nightly.1
|
||||
- 3.14.11-nightly.4
|
||||
- 3.14.11-nightly.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ arguments and it will create zip file that OpenPype can use.
|
|||
Building documentation
|
||||
----------------------
|
||||
|
||||
Top build API documentation, run `.\tools\make_docs(.ps1|.sh)`. It will create html documentation
|
||||
To build API documentation, run `.\tools\make_docs(.ps1|.sh)`. It will create html documentation
|
||||
from current sources in `.\docs\build`.
|
||||
|
||||
**Note that it needs existing virtual environment.**
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype.hosts.blender.api import lib
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreatePointcache(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreatePointcache(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "pointcacheMain"
|
||||
|
|
@ -16,20 +16,36 @@ class CreatePointcache(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "gears"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(collection, self.data)
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return collection
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -60,18 +60,29 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
|
||||
imported = lib.get_selection()
|
||||
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
# Use first EMPTY without parent as container
|
||||
container = next(
|
||||
(obj for obj in imported
|
||||
if obj.type == "EMPTY" and not obj.parent),
|
||||
None
|
||||
)
|
||||
|
||||
objects = []
|
||||
if container:
|
||||
nodes = list(container.children)
|
||||
|
||||
for obj in imported:
|
||||
obj.parent = asset_group
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
for obj in imported:
|
||||
objects.append(obj)
|
||||
imported.extend(list(obj.children))
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
objects.reverse()
|
||||
objects.extend(nodes)
|
||||
for obj in nodes:
|
||||
objects.extend(obj.children_recursive)
|
||||
else:
|
||||
for obj in imported:
|
||||
obj.parent = asset_group
|
||||
objects = imported
|
||||
|
||||
for obj in objects:
|
||||
# Unlink the object from all collections
|
||||
|
|
@ -137,6 +148,7 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
bpy.context.scene.collection.children.link(containers)
|
||||
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
containers.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name)
|
||||
|
|
|
|||
|
|
@ -19,85 +19,51 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
@staticmethod
|
||||
def get_asset_groups() -> Generator:
|
||||
"""Return all 'model' collections.
|
||||
|
||||
Check if the family is 'model' and if it doesn't have the
|
||||
representation set. If the representation is set, it is a loaded model
|
||||
and we don't want to publish it.
|
||||
"""Return all instances that are empty objects asset groups.
|
||||
"""
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
for obj in instances.objects:
|
||||
avalon_prop = obj.get(AVALON_PROPERTY) or dict()
|
||||
for obj in list(instances.objects) + list(instances.children):
|
||||
avalon_prop = obj.get(AVALON_PROPERTY) or {}
|
||||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield obj
|
||||
|
||||
@staticmethod
|
||||
def get_collections() -> Generator:
|
||||
"""Return all 'model' collections.
|
||||
|
||||
Check if the family is 'model' and if it doesn't have the
|
||||
representation set. If the representation is set, it is a loaded model
|
||||
and we don't want to publish it.
|
||||
"""
|
||||
for collection in bpy.data.collections:
|
||||
avalon_prop = collection.get(AVALON_PROPERTY) or dict()
|
||||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield collection
|
||||
def create_instance(context, group):
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
return context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
|
||||
def process(self, context):
|
||||
"""Collect the models from the current Blender scene."""
|
||||
asset_groups = self.get_asset_groups()
|
||||
collections = self.get_collections()
|
||||
|
||||
for group in asset_groups:
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
instance = context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
objects = list(group.children)
|
||||
members = set()
|
||||
for obj in objects:
|
||||
objects.extend(list(obj.children))
|
||||
members.add(obj)
|
||||
members.add(group)
|
||||
instance[:] = list(members)
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
instance = self.create_instance(context, group)
|
||||
members = []
|
||||
if isinstance(group, bpy.types.Collection):
|
||||
members = list(group.objects)
|
||||
family = instance.data["family"]
|
||||
if family == "animation":
|
||||
for obj in group.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
members.extend(
|
||||
child for child in obj.children
|
||||
if child.type == 'ARMATURE')
|
||||
else:
|
||||
members = group.children_recursive
|
||||
|
||||
for collection in collections:
|
||||
avalon_prop = collection[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
instance = context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
members = list(collection.objects)
|
||||
if family == "animation":
|
||||
for obj in collection.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
for child in obj.children:
|
||||
if child.type == 'ARMATURE':
|
||||
members.append(child)
|
||||
members.append(collection)
|
||||
members.append(group)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ class ExtractABC(publish.Extractor):
|
|||
|
||||
label = "Extract ABC"
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache"]
|
||||
optional = True
|
||||
families = ["pointcache"]
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
|
|
@ -62,3 +61,12 @@ class ExtractABC(publish.Extractor):
|
|||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
||||
|
||||
class ExtractModelABC(ExtractABC):
|
||||
"""Extract model as ABC."""
|
||||
|
||||
label = "Extract Model ABC"
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
optional = True
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action", "layout", "blendScene",
|
||||
"render"]
|
||||
"pointcache", "render"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -234,27 +234,40 @@ def reset_scene_resolution():
|
|||
set_scene_resolution(width, height)
|
||||
|
||||
|
||||
def get_frame_range() -> Union[Dict[str, Any], None]:
|
||||
def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]:
|
||||
"""Get the current assets frame range and handles.
|
||||
|
||||
Args:
|
||||
asset_doc (dict): Asset Entity Data
|
||||
|
||||
Returns:
|
||||
dict: with frame start, frame end, handle start, handle end.
|
||||
"""
|
||||
# Set frame start/end
|
||||
asset = get_current_project_asset()
|
||||
frame_start = asset["data"].get("frameStart")
|
||||
frame_end = asset["data"].get("frameEnd")
|
||||
if asset_doc is None:
|
||||
asset_doc = get_current_project_asset()
|
||||
|
||||
data = asset_doc["data"]
|
||||
frame_start = data.get("frameStart")
|
||||
frame_end = data.get("frameEnd")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
return
|
||||
return {}
|
||||
|
||||
frame_start = int(frame_start)
|
||||
frame_end = int(frame_end)
|
||||
handle_start = int(data.get("handleStart", 0))
|
||||
handle_end = int(data.get("handleEnd", 0))
|
||||
frame_start_handle = frame_start - handle_start
|
||||
frame_end_handle = frame_end + handle_end
|
||||
|
||||
handle_start = asset["data"].get("handleStart", 0)
|
||||
handle_end = asset["data"].get("handleEnd", 0)
|
||||
return {
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end
|
||||
"handleEnd": handle_end,
|
||||
"frameStartHandle": frame_start_handle,
|
||||
"frameEndHandle": frame_end_handle,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -274,12 +287,11 @@ def reset_frame_range(fps: bool = True):
|
|||
fps_number = float(data_fps["data"]["fps"])
|
||||
rt.frameRate = fps_number
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
||||
set_timeline(frame_start_handle, frame_end_handle)
|
||||
set_render_frame_range(frame_start_handle, frame_end_handle)
|
||||
|
||||
set_timeline(
|
||||
frame_range["frameStartHandle"], frame_range["frameEndHandle"])
|
||||
set_render_frame_range(
|
||||
frame_range["frameStartHandle"], frame_range["frameEndHandle"])
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
|
|
|
|||
22
openpype/hosts/max/plugins/publish/collect_frame_range.py
Normal file
22
openpype/hosts/max/plugins/publish/collect_frame_range.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class CollectFrameRange(pyblish.api.InstancePlugin):
|
||||
"""Collect Frame Range."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Collect Frame Range"
|
||||
hosts = ['max']
|
||||
families = ["camera", "maxrender",
|
||||
"pointcache", "pointcloud",
|
||||
"review", "redshiftproxy"]
|
||||
|
||||
def process(self, instance):
|
||||
if instance.data["family"] == "maxrender":
|
||||
instance.data["frameStartHandle"] = int(rt.rendStart)
|
||||
instance.data["frameEndHandle"] = int(rt.rendEnd)
|
||||
else:
|
||||
instance.data["frameStartHandle"] = int(rt.animationRange.start)
|
||||
instance.data["frameEndHandle"] = int(rt.animationRange.end)
|
||||
|
|
@ -14,7 +14,7 @@ from openpype.client import get_last_version_by_subset_name
|
|||
class CollectRender(pyblish.api.InstancePlugin):
|
||||
"""Collect Render for Deadline"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
order = pyblish.api.CollectorOrder + 0.02
|
||||
label = "Collect 3dsmax Render Layers"
|
||||
hosts = ['max']
|
||||
families = ["maxrender"]
|
||||
|
|
@ -97,8 +97,8 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
"renderer": renderer,
|
||||
"source": filepath,
|
||||
"plugin": "3dsmax",
|
||||
"frameStart": int(rt.rendStart),
|
||||
"frameEnd": int(rt.rendEnd),
|
||||
"frameStart": instance.data["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"version": version_int,
|
||||
"farm": True
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ class CollectReview(pyblish.api.InstancePlugin,
|
|||
|
||||
general_preview_data = {
|
||||
"review_camera": camera_name,
|
||||
"frameStart": instance.data["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"imageFormat": creator_attrs["imageFormat"],
|
||||
"keepImages": creator_attrs["keepImages"],
|
||||
"frameStart": instance.context.data["frameStart"],
|
||||
"frameEnd": instance.context.data["frameEnd"],
|
||||
"fps": instance.context.data["fps"],
|
||||
"review_width": creator_attrs["review_width"],
|
||||
"review_height": creator_attrs["review_height"],
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
start = float(instance.data.get("frameStartHandle", 1))
|
||||
end = float(instance.data.get("frameEndHandle", 1))
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Extracting Camera ...")
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ class ExtractAlembic(publish.Extractor):
|
|||
families = ["pointcache"]
|
||||
|
||||
def process(self, instance):
|
||||
start = float(instance.data.get("frameStartHandle", 1))
|
||||
end = float(instance.data.get("frameEndHandle", 1))
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
|
||||
self.log.debug("Extracting pointcache ...")
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ class ExtractPointCloud(publish.Extractor):
|
|||
|
||||
def process(self, instance):
|
||||
self.settings = self.get_setting(instance)
|
||||
start = int(instance.context.data.get("frameStart"))
|
||||
end = int(instance.context.data.get("frameEnd"))
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
self.log.info("Extracting PRT...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ class ExtractRedshiftProxy(publish.Extractor):
|
|||
families = ["redshiftproxy"]
|
||||
|
||||
def process(self, instance):
|
||||
start = int(instance.context.data.get("frameStart"))
|
||||
end = int(instance.context.data.get("frameEnd"))
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
|
||||
self.log.debug("Extracting Redshift Proxy...")
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ class ExtractReviewAnimation(publish.Extractor):
|
|||
"ext": instance.data["imageFormat"],
|
||||
"files": filenames,
|
||||
"stagingDir": staging_dir,
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"frameStart": instance.data["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"tags": tags,
|
||||
"preview": True,
|
||||
"camera_name": review_camera
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import tempfile
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.max.api.preview_animation import render_preview_animation
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
)
|
||||
from openpype.hosts.max.api.lib import get_frame_range, set_timeline
|
||||
|
||||
|
||||
class ValidateAnimationTimeline(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Validates Animation Timeline for Preview Animation in Max
|
||||
"""
|
||||
|
||||
label = "Animation Timeline for Review"
|
||||
order = ValidateContentsOrder
|
||||
families = ["review"]
|
||||
hosts = ["max"]
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
if rt.animationRange.start != frame_start_handle or (
|
||||
rt.animationRange.end != frame_end_handle
|
||||
):
|
||||
raise PublishValidationError("Incorrect animation timeline "
|
||||
"set for preview animation.. "
|
||||
"\nYou can use repair action to "
|
||||
"the correct animation timeline")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
set_timeline(frame_start_handle, frame_end_handle)
|
||||
|
|
@ -7,8 +7,10 @@ from openpype.pipeline import (
|
|||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
PublishValidationError,
|
||||
KnownPublishError
|
||||
)
|
||||
from openpype.hosts.max.api.lib import get_frame_range, set_timeline
|
||||
|
||||
|
||||
class ValidateFrameRange(pyblish.api.InstancePlugin,
|
||||
|
|
@ -27,38 +29,60 @@ class ValidateFrameRange(pyblish.api.InstancePlugin,
|
|||
|
||||
label = "Validate Frame Range"
|
||||
order = ValidateContentsOrder
|
||||
families = ["maxrender"]
|
||||
families = ["camera", "maxrender",
|
||||
"pointcache", "pointcloud",
|
||||
"review", "redshiftproxy"]
|
||||
hosts = ["max"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
self.log.info("Skipping validation...")
|
||||
self.log.debug("Skipping Validate Frame Range...")
|
||||
return
|
||||
context = instance.context
|
||||
|
||||
frame_start = int(context.data.get("frameStart"))
|
||||
frame_end = int(context.data.get("frameEnd"))
|
||||
|
||||
inst_frame_start = int(instance.data.get("frameStart"))
|
||||
inst_frame_end = int(instance.data.get("frameEnd"))
|
||||
frame_range = get_frame_range(
|
||||
asset_doc=instance.data["assetEntity"])
|
||||
|
||||
inst_frame_start = instance.data.get("frameStartHandle")
|
||||
inst_frame_end = instance.data.get("frameEndHandle")
|
||||
if inst_frame_start is None or inst_frame_end is None:
|
||||
raise KnownPublishError(
|
||||
"Missing frame start and frame end on "
|
||||
"instance to to validate."
|
||||
)
|
||||
frame_start_handle = frame_range["frameStartHandle"]
|
||||
frame_end_handle = frame_range["frameEndHandle"]
|
||||
errors = []
|
||||
if frame_start != inst_frame_start:
|
||||
if frame_start_handle != inst_frame_start:
|
||||
errors.append(
|
||||
f"Start frame ({inst_frame_start}) on instance does not match " # noqa
|
||||
f"with the start frame ({frame_start}) set on the asset data. ") # noqa
|
||||
if frame_end != inst_frame_end:
|
||||
f"with the start frame ({frame_start_handle}) set on the asset data. ") # noqa
|
||||
if frame_end_handle != inst_frame_end:
|
||||
errors.append(
|
||||
f"End frame ({inst_frame_end}) on instance does not match "
|
||||
f"with the end frame ({frame_start}) from the asset data. ")
|
||||
f"with the end frame ({frame_end_handle}) "
|
||||
"from the asset data. ")
|
||||
|
||||
if errors:
|
||||
errors.append("You can use repair action to fix it.")
|
||||
raise PublishValidationError("\n".join(errors))
|
||||
bullet_point_errors = "\n".join(
|
||||
"- {}".format(error) for error in errors
|
||||
)
|
||||
report = (
|
||||
"Frame range settings are incorrect.\n\n"
|
||||
f"{bullet_point_errors}\n\n"
|
||||
"You can use repair action to fix it."
|
||||
)
|
||||
raise PublishValidationError(report, title="Frame Range incorrect")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
rt.rendStart = instance.context.data.get("frameStart")
|
||||
rt.rendEnd = instance.context.data.get("frameEnd")
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStartHandle"]
|
||||
frame_end_handle = frame_range["frameEndHandle"]
|
||||
|
||||
if instance.data["family"] == "maxrender":
|
||||
rt.rendStart = frame_start_handle
|
||||
rt.rendEnd = frame_end_handle
|
||||
else:
|
||||
set_timeline(frame_start_handle, frame_end_handle)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -6,8 +6,6 @@ Requires:
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
|
||||
class StartTimer(pyblish.api.ContextPlugin):
|
||||
label = "Start Timer"
|
||||
|
|
@ -25,9 +23,9 @@ class StartTimer(pyblish.api.ContextPlugin):
|
|||
self.log.debug("Publish is not affecting running timers.")
|
||||
return
|
||||
|
||||
project_name = legacy_io.active_project()
|
||||
asset_name = legacy_io.Session.get("AVALON_ASSET")
|
||||
task_name = legacy_io.Session.get("AVALON_TASK")
|
||||
project_name = context.data["projectName"]
|
||||
asset_name = context.data.get("asset")
|
||||
task_name = context.data.get("task")
|
||||
if not project_name or not asset_name or not task_name:
|
||||
self.log.info((
|
||||
"Current context does not contain all"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from openpype import PACKAGE_DIR
|
||||
from openpype import PACKAGE_DIR, AYON_SERVER_ENABLED
|
||||
from openpype.lib import get_openpype_execute_args, run_detached_process
|
||||
from openpype.pipeline import load
|
||||
from openpype.pipeline.load import LoadError
|
||||
|
|
@ -32,12 +32,22 @@ class PushToLibraryProject(load.SubsetLoaderPlugin):
|
|||
raise LoadError("Please select only one item")
|
||||
|
||||
context = tuple(filtered_contexts)[0]
|
||||
push_tool_script_path = os.path.join(
|
||||
PACKAGE_DIR,
|
||||
"tools",
|
||||
"push_to_project",
|
||||
"app.py"
|
||||
)
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
push_tool_script_path = os.path.join(
|
||||
PACKAGE_DIR,
|
||||
"tools",
|
||||
"ayon_push_to_project",
|
||||
"main.py"
|
||||
)
|
||||
else:
|
||||
push_tool_script_path = os.path.join(
|
||||
PACKAGE_DIR,
|
||||
"tools",
|
||||
"push_to_project",
|
||||
"app.py"
|
||||
)
|
||||
|
||||
project_doc = context["project"]
|
||||
version_doc = context["version"]
|
||||
project_name = project_doc["name"]
|
||||
|
|
|
|||
|
|
@ -89,10 +89,10 @@
|
|||
"optional": true,
|
||||
"active": false
|
||||
},
|
||||
"ExtractABC": {
|
||||
"ExtractModelABC": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": false
|
||||
"active": true
|
||||
},
|
||||
"ExtractBlendAnimation": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -181,12 +181,12 @@
|
|||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ExtractFBX",
|
||||
"label": "Extract FBX (model and rig)"
|
||||
"key": "ExtractModelABC",
|
||||
"label": "Extract ABC (model)"
|
||||
},
|
||||
{
|
||||
"key": "ExtractABC",
|
||||
"label": "Extract ABC (model and pointcache)"
|
||||
"key": "ExtractFBX",
|
||||
"label": "Extract FBX (model and rig)"
|
||||
},
|
||||
{
|
||||
"key": "ExtractBlendAnimation",
|
||||
|
|
|
|||
6
openpype/tools/ayon_push_to_project/__init__.py
Normal file
6
openpype/tools/ayon_push_to_project/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .control import PushToContextController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PushToContextController",
|
||||
)
|
||||
344
openpype/tools/ayon_push_to_project/control.py
Normal file
344
openpype/tools/ayon_push_to_project/control.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
import threading
|
||||
|
||||
from openpype.client import (
|
||||
get_asset_by_id,
|
||||
get_subset_by_id,
|
||||
get_version_by_id,
|
||||
get_representations,
|
||||
)
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib import prepare_template_data
|
||||
from openpype.lib.events import QueuedEventSystem
|
||||
from openpype.pipeline.create import get_subset_name_template
|
||||
from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel
|
||||
|
||||
from .models import (
|
||||
PushToProjectSelectionModel,
|
||||
UserPublishValuesModel,
|
||||
IntegrateModel,
|
||||
)
|
||||
|
||||
|
||||
class PushToContextController:
|
||||
def __init__(self, project_name=None, version_id=None):
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._integrate_model = IntegrateModel(self)
|
||||
|
||||
self._selection_model = PushToProjectSelectionModel(self)
|
||||
self._user_values = UserPublishValuesModel(self)
|
||||
|
||||
self._src_project_name = None
|
||||
self._src_version_id = None
|
||||
self._src_asset_doc = None
|
||||
self._src_subset_doc = None
|
||||
self._src_version_doc = None
|
||||
self._src_label = None
|
||||
|
||||
self._submission_enabled = False
|
||||
self._process_thread = None
|
||||
self._process_item_id = None
|
||||
|
||||
self.set_source(project_name, version_id)
|
||||
|
||||
# Events system
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
"""Use implemented event system to trigger event."""
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
self._event_system.emit(topic, data, source)
|
||||
|
||||
def register_event_callback(self, topic, callback):
|
||||
self._event_system.add_callback(topic, callback)
|
||||
|
||||
def set_source(self, project_name, version_id):
|
||||
"""Set source project and version.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Source project name.
|
||||
version_id (Union[str, None]): Source version id.
|
||||
"""
|
||||
|
||||
if (
|
||||
project_name == self._src_project_name
|
||||
and version_id == self._src_version_id
|
||||
):
|
||||
return
|
||||
|
||||
self._src_project_name = project_name
|
||||
self._src_version_id = version_id
|
||||
self._src_label = None
|
||||
asset_doc = None
|
||||
subset_doc = None
|
||||
version_doc = None
|
||||
if project_name and version_id:
|
||||
version_doc = get_version_by_id(project_name, version_id)
|
||||
|
||||
if version_doc:
|
||||
subset_doc = get_subset_by_id(project_name, version_doc["parent"])
|
||||
|
||||
if subset_doc:
|
||||
asset_doc = get_asset_by_id(project_name, subset_doc["parent"])
|
||||
|
||||
self._src_asset_doc = asset_doc
|
||||
self._src_subset_doc = subset_doc
|
||||
self._src_version_doc = version_doc
|
||||
if asset_doc:
|
||||
self._user_values.set_new_folder_name(asset_doc["name"])
|
||||
variant = self._get_src_variant()
|
||||
if variant:
|
||||
self._user_values.set_variant(variant)
|
||||
|
||||
comment = version_doc["data"].get("comment")
|
||||
if comment:
|
||||
self._user_values.set_comment(comment)
|
||||
|
||||
self._emit_event(
|
||||
"source.changed",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_id": version_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_source_label(self):
|
||||
"""Get source label.
|
||||
|
||||
Returns:
|
||||
str: Label describing source project and version as path.
|
||||
"""
|
||||
|
||||
if self._src_label is None:
|
||||
self._src_label = self._prepare_source_label()
|
||||
return self._src_label
|
||||
|
||||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_task_items(self, project_name, folder_id, sender=None):
|
||||
return self._hierarchy_model.get_task_items(
|
||||
project_name, folder_id, sender
|
||||
)
|
||||
|
||||
def get_user_values(self):
|
||||
return self._user_values.get_data()
|
||||
|
||||
def set_user_value_folder_name(self, folder_name):
|
||||
self._user_values.set_new_folder_name(folder_name)
|
||||
self._invalidate()
|
||||
|
||||
def set_user_value_variant(self, variant):
|
||||
self._user_values.set_variant(variant)
|
||||
self._invalidate()
|
||||
|
||||
def set_user_value_comment(self, comment):
|
||||
self._user_values.set_comment(comment)
|
||||
self._invalidate()
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
self._selection_model.set_selected_project(project_name)
|
||||
self._invalidate()
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
self._selection_model.set_selected_folder(folder_id)
|
||||
self._invalidate()
|
||||
|
||||
def set_selected_task(self, task_id, task_name):
|
||||
self._selection_model.set_selected_task(task_id, task_name)
|
||||
|
||||
def get_process_item_status(self, item_id):
|
||||
return self._integrate_model.get_item_status(item_id)
|
||||
|
||||
# Processing methods
|
||||
def submit(self, wait=True):
|
||||
if not self._submission_enabled:
|
||||
return
|
||||
|
||||
if self._process_thread is not None:
|
||||
return
|
||||
|
||||
item_id = self._integrate_model.create_process_item(
|
||||
self._src_project_name,
|
||||
self._src_version_id,
|
||||
self._selection_model.get_selected_project_name(),
|
||||
self._selection_model.get_selected_folder_id(),
|
||||
self._selection_model.get_selected_task_name(),
|
||||
self._user_values.variant,
|
||||
comment=self._user_values.comment,
|
||||
new_folder_name=self._user_values.new_folder_name,
|
||||
dst_version=1
|
||||
)
|
||||
|
||||
self._process_item_id = item_id
|
||||
self._emit_event("submit.started")
|
||||
if wait:
|
||||
self._submit_callback()
|
||||
self._process_item_id = None
|
||||
return item_id
|
||||
|
||||
thread = threading.Thread(target=self._submit_callback)
|
||||
self._process_thread = thread
|
||||
thread.start()
|
||||
return item_id
|
||||
|
||||
def wait_for_process_thread(self):
|
||||
if self._process_thread is None:
|
||||
return
|
||||
self._process_thread.join()
|
||||
self._process_thread = None
|
||||
|
||||
def _prepare_source_label(self):
|
||||
if not self._src_project_name or not self._src_version_id:
|
||||
return "Source is not defined"
|
||||
|
||||
asset_doc = self._src_asset_doc
|
||||
if not asset_doc:
|
||||
return "Source is invalid"
|
||||
|
||||
folder_path_parts = list(asset_doc["data"]["parents"])
|
||||
folder_path_parts.append(asset_doc["name"])
|
||||
folder_path = "/".join(folder_path_parts)
|
||||
subset_doc = self._src_subset_doc
|
||||
version_doc = self._src_version_doc
|
||||
return "Source: {}/{}/{}/v{:0>3}".format(
|
||||
self._src_project_name,
|
||||
folder_path,
|
||||
subset_doc["name"],
|
||||
version_doc["name"]
|
||||
)
|
||||
|
||||
def _get_task_info_from_repre_docs(self, asset_doc, repre_docs):
|
||||
asset_tasks = asset_doc["data"].get("tasks") or {}
|
||||
found_comb = []
|
||||
for repre_doc in repre_docs:
|
||||
context = repre_doc["context"]
|
||||
task_info = context.get("task")
|
||||
if task_info is None:
|
||||
continue
|
||||
|
||||
task_name = None
|
||||
task_type = None
|
||||
if isinstance(task_info, str):
|
||||
task_name = task_info
|
||||
asset_task_info = asset_tasks.get(task_info) or {}
|
||||
task_type = asset_task_info.get("type")
|
||||
|
||||
elif isinstance(task_info, dict):
|
||||
task_name = task_info.get("name")
|
||||
task_type = task_info.get("type")
|
||||
|
||||
if task_name and task_type:
|
||||
return task_name, task_type
|
||||
|
||||
if task_name:
|
||||
found_comb.append((task_name, task_type))
|
||||
|
||||
for task_name, task_type in found_comb:
|
||||
return task_name, task_type
|
||||
return None, None
|
||||
|
||||
def _get_src_variant(self):
|
||||
project_name = self._src_project_name
|
||||
version_doc = self._src_version_doc
|
||||
asset_doc = self._src_asset_doc
|
||||
repre_docs = get_representations(
|
||||
project_name, version_ids=[version_doc["_id"]]
|
||||
)
|
||||
task_name, task_type = self._get_task_info_from_repre_docs(
|
||||
asset_doc, repre_docs
|
||||
)
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
subset_doc = self._src_subset_doc
|
||||
family = subset_doc["data"].get("family")
|
||||
if not family:
|
||||
family = subset_doc["data"]["families"][0]
|
||||
template = get_subset_name_template(
|
||||
self._src_project_name,
|
||||
family,
|
||||
task_name,
|
||||
task_type,
|
||||
None,
|
||||
project_settings=project_settings
|
||||
)
|
||||
template_low = template.lower()
|
||||
variant_placeholder = "{variant}"
|
||||
if (
|
||||
variant_placeholder not in template_low
|
||||
or (not task_name and "{task" in template_low)
|
||||
):
|
||||
return ""
|
||||
|
||||
idx = template_low.index(variant_placeholder)
|
||||
template_s = template[:idx]
|
||||
template_e = template[idx + len(variant_placeholder):]
|
||||
fill_data = prepare_template_data({
|
||||
"family": family,
|
||||
"task": task_name
|
||||
})
|
||||
try:
|
||||
subset_s = template_s.format(**fill_data)
|
||||
subset_e = template_e.format(**fill_data)
|
||||
except Exception as exc:
|
||||
print("Failed format", exc)
|
||||
return ""
|
||||
|
||||
subset_name = self._src_subset_doc["name"]
|
||||
if (
|
||||
(subset_s and not subset_name.startswith(subset_s))
|
||||
or (subset_e and not subset_name.endswith(subset_e))
|
||||
):
|
||||
return ""
|
||||
|
||||
if subset_s:
|
||||
subset_name = subset_name[len(subset_s):]
|
||||
if subset_e:
|
||||
subset_name = subset_name[:len(subset_e)]
|
||||
return subset_name
|
||||
|
||||
def _check_submit_validations(self):
|
||||
if not self._user_values.is_valid:
|
||||
return False
|
||||
|
||||
if not self._selection_model.get_selected_project_name():
|
||||
return False
|
||||
|
||||
if (
|
||||
not self._user_values.new_folder_name
|
||||
and not self._selection_model.get_selected_folder_id()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _invalidate(self):
|
||||
submission_enabled = self._check_submit_validations()
|
||||
if submission_enabled == self._submission_enabled:
|
||||
return
|
||||
self._submission_enabled = submission_enabled
|
||||
self._emit_event(
|
||||
"submission.enabled.changed",
|
||||
{"enabled": submission_enabled}
|
||||
)
|
||||
|
||||
def _submit_callback(self):
|
||||
process_item_id = self._process_item_id
|
||||
if process_item_id is None:
|
||||
return
|
||||
self._integrate_model.integrate_item(process_item_id)
|
||||
self._emit_event("submit.finished", {})
|
||||
if process_item_id == self._process_item_id:
|
||||
self._process_item_id = None
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
||||
def _create_event_system(self):
|
||||
return QueuedEventSystem()
|
||||
32
openpype/tools/ayon_push_to_project/main.py
Normal file
32
openpype/tools/ayon_push_to_project/main.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import click
|
||||
|
||||
from openpype.tools.utils import get_openpype_qt_app
|
||||
from openpype.tools.ayon_push_to_project.ui import PushToContextSelectWindow
|
||||
|
||||
|
||||
def main_show(project_name, version_id):
|
||||
app = get_openpype_qt_app()
|
||||
|
||||
window = PushToContextSelectWindow()
|
||||
window.show()
|
||||
window.set_source(project_name, version_id)
|
||||
|
||||
app.exec_()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--project", help="Source project name")
|
||||
@click.option("--version", help="Source version id")
|
||||
def main(project, version):
|
||||
"""Run PushToProject tool to integrate version in different project.
|
||||
|
||||
Args:
|
||||
project (str): Source project name.
|
||||
version (str): Version id.
|
||||
"""
|
||||
|
||||
main_show(project, version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
openpype/tools/ayon_push_to_project/models/__init__.py
Normal file
10
openpype/tools/ayon_push_to_project/models/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .selection import PushToProjectSelectionModel
|
||||
from .user_values import UserPublishValuesModel
|
||||
from .integrate import IntegrateModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PushToProjectSelectionModel",
|
||||
"UserPublishValuesModel",
|
||||
"IntegrateModel",
|
||||
)
|
||||
1214
openpype/tools/ayon_push_to_project/models/integrate.py
Normal file
1214
openpype/tools/ayon_push_to_project/models/integrate.py
Normal file
File diff suppressed because it is too large
Load diff
72
openpype/tools/ayon_push_to_project/models/selection.py
Normal file
72
openpype/tools/ayon_push_to_project/models/selection.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
class PushToProjectSelectionModel(object):
|
||||
"""Model handling selection changes.
|
||||
|
||||
Triggering events:
|
||||
- "selection.project.changed"
|
||||
- "selection.folder.changed"
|
||||
- "selection.task.changed"
|
||||
"""
|
||||
|
||||
event_source = "push-to-project.selection.model"
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
self._project_name = None
|
||||
self._folder_id = None
|
||||
self._task_name = None
|
||||
self._task_id = None
|
||||
|
||||
def get_selected_project_name(self):
|
||||
return self._project_name
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
if project_name == self._project_name:
|
||||
return
|
||||
|
||||
self._project_name = project_name
|
||||
self._controller.emit_event(
|
||||
"selection.project.changed",
|
||||
{"project_name": project_name},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
return self._folder_id
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
if folder_id == self._folder_id:
|
||||
return
|
||||
|
||||
self._folder_id = folder_id
|
||||
self._controller.emit_event(
|
||||
"selection.folder.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"folder_id": folder_id,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_task_name(self):
|
||||
return self._task_name
|
||||
|
||||
def get_selected_task_id(self):
|
||||
return self._task_id
|
||||
|
||||
def set_selected_task(self, task_id, task_name):
|
||||
if task_id == self._task_id:
|
||||
return
|
||||
|
||||
self._task_name = task_name
|
||||
self._task_id = task_id
|
||||
self._controller.emit_event(
|
||||
"selection.task.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"folder_id": self._folder_id,
|
||||
"task_name": task_name,
|
||||
"task_id": task_id,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
110
openpype/tools/ayon_push_to_project/models/user_values.py
Normal file
110
openpype/tools/ayon_push_to_project/models/user_values.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import re
|
||||
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
|
||||
|
||||
class UserPublishValuesModel:
|
||||
"""Helper object to validate values required for push to different project.
|
||||
|
||||
Args:
|
||||
controller (PushToContextController): Event system to catch
|
||||
and emit events.
|
||||
"""
|
||||
|
||||
folder_name_regex = re.compile("^[a-zA-Z0-9_.]+$")
|
||||
variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS))
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._new_folder_name = None
|
||||
self._variant = None
|
||||
self._comment = None
|
||||
self._is_variant_valid = False
|
||||
self._is_new_folder_name_valid = False
|
||||
|
||||
self.set_new_folder_name("")
|
||||
self.set_variant("")
|
||||
self.set_comment("")
|
||||
|
||||
@property
|
||||
def new_folder_name(self):
|
||||
return self._new_folder_name
|
||||
|
||||
@property
|
||||
def variant(self):
|
||||
return self._variant
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
return self._comment
|
||||
|
||||
@property
|
||||
def is_variant_valid(self):
|
||||
return self._is_variant_valid
|
||||
|
||||
@property
|
||||
def is_new_folder_name_valid(self):
|
||||
return self._is_new_folder_name_valid
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.is_variant_valid and self.is_new_folder_name_valid
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
"new_folder_name": self._new_folder_name,
|
||||
"variant": self._variant,
|
||||
"comment": self._comment,
|
||||
"is_variant_valid": self._is_variant_valid,
|
||||
"is_new_folder_name_valid": self._is_new_folder_name_valid,
|
||||
"is_valid": self.is_valid
|
||||
}
|
||||
|
||||
def set_variant(self, variant):
|
||||
if variant == self._variant:
|
||||
return
|
||||
|
||||
self._variant = variant
|
||||
is_valid = False
|
||||
if variant:
|
||||
is_valid = self.variant_regex.match(variant) is not None
|
||||
self._is_variant_valid = is_valid
|
||||
|
||||
self._controller.emit_event(
|
||||
"variant.changed",
|
||||
{
|
||||
"variant": variant,
|
||||
"is_valid": self._is_variant_valid,
|
||||
},
|
||||
"user_values"
|
||||
)
|
||||
|
||||
def set_new_folder_name(self, folder_name):
|
||||
if self._new_folder_name == folder_name:
|
||||
return
|
||||
|
||||
self._new_folder_name = folder_name
|
||||
is_valid = True
|
||||
if folder_name:
|
||||
is_valid = (
|
||||
self.folder_name_regex.match(folder_name) is not None
|
||||
)
|
||||
self._is_new_folder_name_valid = is_valid
|
||||
self._controller.emit_event(
|
||||
"new_folder_name.changed",
|
||||
{
|
||||
"new_folder_name": self._new_folder_name,
|
||||
"is_valid": self._is_new_folder_name_valid,
|
||||
},
|
||||
"user_values"
|
||||
)
|
||||
|
||||
def set_comment(self, comment):
|
||||
if comment == self._comment:
|
||||
return
|
||||
self._comment = comment
|
||||
self._controller.emit_event(
|
||||
"comment.changed",
|
||||
{"comment": comment},
|
||||
"user_values"
|
||||
)
|
||||
6
openpype/tools/ayon_push_to_project/ui/__init__.py
Normal file
6
openpype/tools/ayon_push_to_project/ui/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .window import PushToContextSelectWindow
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PushToContextSelectWindow",
|
||||
)
|
||||
432
openpype/tools/ayon_push_to_project/ui/window.py
Normal file
432
openpype/tools/ayon_push_to_project/ui/window.py
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from openpype.style import load_stylesheet, get_app_icon_path
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
SeparatorWidget,
|
||||
set_style_property,
|
||||
)
|
||||
from openpype.tools.ayon_utils.widgets import (
|
||||
ProjectsCombobox,
|
||||
FoldersWidget,
|
||||
TasksWidget,
|
||||
)
|
||||
from openpype.tools.ayon_push_to_project.control import (
|
||||
PushToContextController,
|
||||
)
|
||||
|
||||
|
||||
class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||
def __init__(self, controller=None):
|
||||
super(PushToContextSelectWindow, self).__init__()
|
||||
if controller is None:
|
||||
controller = PushToContextController()
|
||||
self._controller = controller
|
||||
|
||||
self.setWindowTitle("Push to project (select context)")
|
||||
self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
|
||||
|
||||
main_context_widget = QtWidgets.QWidget(self)
|
||||
|
||||
header_widget = QtWidgets.QWidget(main_context_widget)
|
||||
|
||||
header_label = QtWidgets.QLabel(
|
||||
controller.get_source_label(),
|
||||
header_widget
|
||||
)
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(header_label)
|
||||
|
||||
main_splitter = QtWidgets.QSplitter(
|
||||
QtCore.Qt.Horizontal, main_context_widget
|
||||
)
|
||||
|
||||
context_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
projects_combobox = ProjectsCombobox(controller, context_widget)
|
||||
projects_combobox.set_select_item_visible(True)
|
||||
projects_combobox.set_standard_filter_enabled(True)
|
||||
|
||||
context_splitter = QtWidgets.QSplitter(
|
||||
QtCore.Qt.Vertical, context_widget
|
||||
)
|
||||
|
||||
folders_widget = FoldersWidget(controller, context_splitter)
|
||||
folders_widget.set_deselectable(True)
|
||||
tasks_widget = TasksWidget(controller, context_splitter)
|
||||
|
||||
context_splitter.addWidget(folders_widget)
|
||||
context_splitter.addWidget(tasks_widget)
|
||||
|
||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||
context_layout.addWidget(projects_combobox, 0)
|
||||
context_layout.addWidget(context_splitter, 1)
|
||||
|
||||
# --- Inputs widget ---
|
||||
inputs_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
folder_name_input = PlaceholderLineEdit(inputs_widget)
|
||||
folder_name_input.setPlaceholderText("< Name of new folder >")
|
||||
folder_name_input.setObjectName("ValidatedLineEdit")
|
||||
|
||||
variant_input = PlaceholderLineEdit(inputs_widget)
|
||||
variant_input.setPlaceholderText("< Variant >")
|
||||
variant_input.setObjectName("ValidatedLineEdit")
|
||||
|
||||
comment_input = PlaceholderLineEdit(inputs_widget)
|
||||
comment_input.setPlaceholderText("< Publish comment >")
|
||||
|
||||
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
|
||||
inputs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
inputs_layout.addRow("New folder name", folder_name_input)
|
||||
inputs_layout.addRow("Variant", variant_input)
|
||||
inputs_layout.addRow("Comment", comment_input)
|
||||
|
||||
main_splitter.addWidget(context_widget)
|
||||
main_splitter.addWidget(inputs_widget)
|
||||
|
||||
# --- Buttons widget ---
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
|
||||
publish_btn = QtWidgets.QPushButton("Publish", btns_widget)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(cancel_btn, 0)
|
||||
btns_layout.addWidget(publish_btn, 0)
|
||||
|
||||
sep_1 = SeparatorWidget(parent=main_context_widget)
|
||||
sep_2 = SeparatorWidget(parent=main_context_widget)
|
||||
main_context_layout = QtWidgets.QVBoxLayout(main_context_widget)
|
||||
main_context_layout.addWidget(header_widget, 0)
|
||||
main_context_layout.addWidget(sep_1, 0)
|
||||
main_context_layout.addWidget(main_splitter, 1)
|
||||
main_context_layout.addWidget(sep_2, 0)
|
||||
main_context_layout.addWidget(btns_widget, 0)
|
||||
|
||||
# NOTE This was added in hurry
|
||||
# - should be reorganized and changed styles
|
||||
overlay_widget = QtWidgets.QFrame(self)
|
||||
overlay_widget.setObjectName("OverlayFrame")
|
||||
|
||||
overlay_label = QtWidgets.QLabel(overlay_widget)
|
||||
overlay_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
overlay_btns_widget = QtWidgets.QWidget(overlay_widget)
|
||||
overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
# Add try again button (requires changes in controller)
|
||||
overlay_try_btn = QtWidgets.QPushButton(
|
||||
"Try again", overlay_btns_widget
|
||||
)
|
||||
overlay_close_btn = QtWidgets.QPushButton(
|
||||
"Close", overlay_btns_widget
|
||||
)
|
||||
|
||||
overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget)
|
||||
overlay_btns_layout.addStretch(1)
|
||||
overlay_btns_layout.addWidget(overlay_try_btn, 0)
|
||||
overlay_btns_layout.addWidget(overlay_close_btn, 0)
|
||||
overlay_btns_layout.addStretch(1)
|
||||
|
||||
overlay_layout = QtWidgets.QVBoxLayout(overlay_widget)
|
||||
overlay_layout.addWidget(overlay_label, 0)
|
||||
overlay_layout.addWidget(overlay_btns_widget, 0)
|
||||
overlay_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
main_layout = QtWidgets.QStackedLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(main_context_widget)
|
||||
main_layout.addWidget(overlay_widget)
|
||||
main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
|
||||
main_layout.setCurrentWidget(main_context_widget)
|
||||
|
||||
show_timer = QtCore.QTimer()
|
||||
show_timer.setInterval(0)
|
||||
|
||||
main_thread_timer = QtCore.QTimer()
|
||||
main_thread_timer.setInterval(10)
|
||||
|
||||
user_input_changed_timer = QtCore.QTimer()
|
||||
user_input_changed_timer.setInterval(200)
|
||||
user_input_changed_timer.setSingleShot(True)
|
||||
|
||||
main_thread_timer.timeout.connect(self._on_main_thread_timer)
|
||||
show_timer.timeout.connect(self._on_show_timer)
|
||||
user_input_changed_timer.timeout.connect(self._on_user_input_timer)
|
||||
folder_name_input.textChanged.connect(self._on_new_asset_change)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
comment_input.textChanged.connect(self._on_comment_change)
|
||||
|
||||
publish_btn.clicked.connect(self._on_select_click)
|
||||
cancel_btn.clicked.connect(self._on_close_click)
|
||||
overlay_close_btn.clicked.connect(self._on_close_click)
|
||||
overlay_try_btn.clicked.connect(self._on_try_again_click)
|
||||
|
||||
controller.register_event_callback(
|
||||
"new_folder_name.changed",
|
||||
self._on_controller_new_asset_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"variant.changed", self._on_controller_variant_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"comment.changed", self._on_controller_comment_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"submission.enabled.changed", self._on_submission_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"source.changed", self._on_controller_source_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"submit.started", self._on_controller_submit_start
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"submit.finished", self._on_controller_submit_end
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"push.message.added", self._on_push_message
|
||||
)
|
||||
|
||||
self._main_layout = main_layout
|
||||
|
||||
self._main_context_widget = main_context_widget
|
||||
|
||||
self._header_label = header_label
|
||||
self._main_splitter = main_splitter
|
||||
|
||||
self._projects_combobox = projects_combobox
|
||||
self._folders_widget = folders_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._variant_input = variant_input
|
||||
self._folder_name_input = folder_name_input
|
||||
self._comment_input = comment_input
|
||||
|
||||
self._publish_btn = publish_btn
|
||||
|
||||
self._overlay_widget = overlay_widget
|
||||
self._overlay_close_btn = overlay_close_btn
|
||||
self._overlay_try_btn = overlay_try_btn
|
||||
self._overlay_label = overlay_label
|
||||
|
||||
self._user_input_changed_timer = user_input_changed_timer
|
||||
# Store current value on input text change
|
||||
# The value is unset when is passed to controller
|
||||
# The goal is to have controll over changes happened during user change
|
||||
# in UI and controller auto-changes
|
||||
self._variant_input_text = None
|
||||
self._new_folder_name_input_text = None
|
||||
self._comment_input_text = None
|
||||
|
||||
self._first_show = True
|
||||
self._show_timer = show_timer
|
||||
self._show_counter = 0
|
||||
|
||||
self._main_thread_timer = main_thread_timer
|
||||
self._main_thread_timer_can_stop = True
|
||||
self._last_submit_message = None
|
||||
self._process_item_id = None
|
||||
|
||||
self._variant_is_valid = None
|
||||
self._folder_is_valid = None
|
||||
|
||||
publish_btn.setEnabled(False)
|
||||
overlay_close_btn.setVisible(False)
|
||||
overlay_try_btn.setVisible(False)
|
||||
|
||||
# Support of public api function of controller
|
||||
def set_source(self, project_name, version_id):
|
||||
"""Set source project and version.
|
||||
|
||||
Call the method on controller.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Name of project.
|
||||
version_id (Union[str, None]): Version id.
|
||||
"""
|
||||
|
||||
self._controller.set_source(project_name, version_id)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(PushToContextSelectWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
def refresh(self):
|
||||
user_values = self._controller.get_user_values()
|
||||
new_folder_name = user_values["new_folder_name"]
|
||||
variant = user_values["variant"]
|
||||
self._folder_name_input.setText(new_folder_name or "")
|
||||
self._variant_input.setText(variant or "")
|
||||
self._invalidate_variant(user_values["is_variant_valid"])
|
||||
self._invalidate_new_folder_name(
|
||||
new_folder_name, user_values["is_new_folder_name_valid"]
|
||||
)
|
||||
|
||||
self._projects_combobox.refresh()
|
||||
|
||||
def _on_first_show(self):
|
||||
width = 740
|
||||
height = 640
|
||||
inputs_width = 360
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
self.resize(width, height)
|
||||
self._main_splitter.setSizes([width - inputs_width, inputs_width])
|
||||
self._show_timer.start()
|
||||
|
||||
def _on_show_timer(self):
|
||||
if self._show_counter < 3:
|
||||
self._show_counter += 1
|
||||
return
|
||||
self._show_timer.stop()
|
||||
|
||||
self._show_counter = 0
|
||||
|
||||
self.refresh()
|
||||
|
||||
def _on_new_asset_change(self, text):
|
||||
self._new_folder_name_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_variant_change(self, text):
|
||||
self._variant_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_comment_change(self, text):
|
||||
self._comment_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_user_input_timer(self):
|
||||
folder_name = self._new_folder_name_input_text
|
||||
if folder_name is not None:
|
||||
self._new_folder_name_input_text = None
|
||||
self._controller.set_user_value_folder_name(folder_name)
|
||||
|
||||
variant = self._variant_input_text
|
||||
if variant is not None:
|
||||
self._variant_input_text = None
|
||||
self._controller.set_user_value_variant(variant)
|
||||
|
||||
comment = self._comment_input_text
|
||||
if comment is not None:
|
||||
self._comment_input_text = None
|
||||
self._controller.set_user_value_comment(comment)
|
||||
|
||||
def _on_controller_new_asset_change(self, event):
|
||||
folder_name = event["new_folder_name"]
|
||||
if (
|
||||
self._new_folder_name_input_text is None
|
||||
and folder_name != self._folder_name_input.text()
|
||||
):
|
||||
self._folder_name_input.setText(folder_name)
|
||||
|
||||
self._invalidate_new_folder_name(folder_name, event["is_valid"])
|
||||
|
||||
def _on_controller_variant_change(self, event):
|
||||
is_valid = event["is_valid"]
|
||||
variant = event["variant"]
|
||||
if (
|
||||
self._variant_input_text is None
|
||||
and variant != self._variant_input.text()
|
||||
):
|
||||
self._variant_input.setText(variant)
|
||||
|
||||
self._invalidate_variant(is_valid)
|
||||
|
||||
def _on_controller_comment_change(self, event):
|
||||
comment = event["comment"]
|
||||
if (
|
||||
self._comment_input_text is None
|
||||
and comment != self._comment_input.text()
|
||||
):
|
||||
self._comment_input.setText(comment)
|
||||
|
||||
def _on_controller_source_change(self):
|
||||
self._header_label.setText(self._controller.get_source_label())
|
||||
|
||||
def _invalidate_new_folder_name(self, folder_name, is_valid):
|
||||
self._tasks_widget.setVisible(not folder_name)
|
||||
if self._folder_is_valid is is_valid:
|
||||
return
|
||||
self._folder_is_valid = is_valid
|
||||
state = ""
|
||||
if folder_name:
|
||||
if is_valid is True:
|
||||
state = "valid"
|
||||
elif is_valid is False:
|
||||
state = "invalid"
|
||||
set_style_property(
|
||||
self._folder_name_input, "state", state
|
||||
)
|
||||
|
||||
def _invalidate_variant(self, is_valid):
|
||||
if self._variant_is_valid is is_valid:
|
||||
return
|
||||
self._variant_is_valid = is_valid
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(self._variant_input, "state", state)
|
||||
|
||||
def _on_submission_change(self, event):
|
||||
self._publish_btn.setEnabled(event["enabled"])
|
||||
|
||||
def _on_close_click(self):
|
||||
self.close()
|
||||
|
||||
def _on_select_click(self):
|
||||
self._process_item_id = self._controller.submit(wait=False)
|
||||
|
||||
def _on_try_again_click(self):
|
||||
self._process_item_id = None
|
||||
self._last_submit_message = None
|
||||
|
||||
self._overlay_close_btn.setVisible(False)
|
||||
self._overlay_try_btn.setVisible(False)
|
||||
self._main_layout.setCurrentWidget(self._main_context_widget)
|
||||
|
||||
def _on_main_thread_timer(self):
|
||||
if self._last_submit_message:
|
||||
self._overlay_label.setText(self._last_submit_message)
|
||||
self._last_submit_message = None
|
||||
|
||||
process_status = self._controller.get_process_item_status(
|
||||
self._process_item_id
|
||||
)
|
||||
push_failed = process_status["failed"]
|
||||
fail_traceback = process_status["full_traceback"]
|
||||
if self._main_thread_timer_can_stop:
|
||||
self._main_thread_timer.stop()
|
||||
self._overlay_close_btn.setVisible(True)
|
||||
if push_failed and not fail_traceback:
|
||||
self._overlay_try_btn.setVisible(True)
|
||||
|
||||
if push_failed:
|
||||
message = "Push Failed:\n{}".format(process_status["fail_reason"])
|
||||
if fail_traceback:
|
||||
message += "\n{}".format(fail_traceback)
|
||||
self._overlay_label.setText(message)
|
||||
set_style_property(self._overlay_close_btn, "state", "error")
|
||||
|
||||
if self._main_thread_timer_can_stop:
|
||||
# Join thread in controller
|
||||
self._controller.wait_for_process_thread()
|
||||
# Reset process item to None
|
||||
self._process_item_id = None
|
||||
|
||||
def _on_controller_submit_start(self):
|
||||
self._main_thread_timer_can_stop = False
|
||||
self._main_thread_timer.start()
|
||||
self._main_layout.setCurrentWidget(self._overlay_widget)
|
||||
self._overlay_label.setText("Submittion started")
|
||||
|
||||
def _on_controller_submit_end(self):
|
||||
self._main_thread_timer_can_stop = True
|
||||
|
||||
def _on_push_message(self, event):
|
||||
self._last_submit_message = event["message"]
|
||||
|
|
@ -1051,6 +1051,11 @@ class ProjectPushItemProcess:
|
|||
repre_format_data["ext"] = ext[1:]
|
||||
break
|
||||
|
||||
# Re-use 'output' from source representation
|
||||
repre_output_name = repre_doc["context"].get("output")
|
||||
if repre_output_name is not None:
|
||||
repre_format_data["output"] = repre_output_name
|
||||
|
||||
template_obj = anatomy.templates_obj[template_name]["folder"]
|
||||
folder_path = template_obj.format_strict(formatting_data)
|
||||
repre_context = folder_path.used_values
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.17.3"
|
||||
__version__ = "3.17.4-nightly.1"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=ValidatePluginModel,
|
||||
title="Extract FBX"
|
||||
)
|
||||
ExtractABC: ValidatePluginModel = Field(
|
||||
ExtractModelABC: ValidatePluginModel = Field(
|
||||
default_factory=ValidatePluginModel,
|
||||
title="Extract ABC"
|
||||
)
|
||||
|
|
@ -197,10 +197,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = {
|
|||
"optional": True,
|
||||
"active": False
|
||||
},
|
||||
"ExtractABC": {
|
||||
"ExtractModelABC": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": False
|
||||
"active": True
|
||||
},
|
||||
"ExtractBlendAnimation": {
|
||||
"enabled": True,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ aiohttp_json_rpc = "*" # TVPaint server
|
|||
aiohttp-middlewares = "^2.0.0"
|
||||
wsrpc_aiohttp = "^3.1.1" # websocket server
|
||||
clique = "1.6.*"
|
||||
shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"}
|
||||
gazu = "^0.9.3"
|
||||
google-api-python-client = "^1.12.8" # sync server google support (should be separate?)
|
||||
jsonschema = "^2.6.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue