mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into feature/1377-hiero-publish-with-retiming
This commit is contained in:
commit
0eeeb19ec8
68 changed files with 5601 additions and 3153 deletions
|
|
@ -203,6 +203,12 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
openpype_version.staging = True
|
||||
return openpype_version
|
||||
|
||||
def __hash__(self):
|
||||
if self.path:
|
||||
return hash(self.path)
|
||||
else:
|
||||
return hash(str(self))
|
||||
|
||||
|
||||
class BootstrapRepos:
|
||||
"""Class for bootstrapping local OpenPype installation.
|
||||
|
|
@ -650,6 +656,9 @@ class BootstrapRepos:
|
|||
v for v in openpype_versions if v.path.suffix != ".zip"
|
||||
]
|
||||
|
||||
# remove duplicates
|
||||
openpype_versions = list(set(openpype_versions))
|
||||
|
||||
return openpype_versions
|
||||
|
||||
def process_entered_location(self, location: str) -> Union[Path, None]:
|
||||
|
|
|
|||
34
openpype/hooks/pre_mac_launch.py
Normal file
34
openpype/hooks/pre_mac_launch.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class LaunchWithTerminal(PreLaunchHook):
|
||||
"""Mac specific pre arguments for application.
|
||||
|
||||
Mac applications should be launched using "open" argument which is internal
|
||||
callbacks to open executable. We also add argument "-a" to tell it's
|
||||
application open. This is used only for executables ending with ".app". It
|
||||
is expected that these executables lead to app packages.
|
||||
"""
|
||||
order = 1000
|
||||
|
||||
platforms = ["darwin"]
|
||||
|
||||
def execute(self):
|
||||
executable = str(self.launch_context.executable)
|
||||
# Skip executables not ending with ".app" or that are not folder
|
||||
if not executable.endswith(".app") or not os.path.isdir(executable):
|
||||
return
|
||||
|
||||
# Check if first argument match executable path
|
||||
# - Few applications are not executed directly but through OpenPype
|
||||
# process (Photoshop, AfterEffects, Harmony, ...). These should not
|
||||
# use `open`.
|
||||
if self.launch_context.launch_args[0] != executable:
|
||||
return
|
||||
|
||||
# Tell `open` to pass arguments if there are any
|
||||
if len(self.launch_context.launch_args) > 1:
|
||||
self.launch_context.launch_args.insert(1, "--args")
|
||||
# Prepend open arguments
|
||||
self.launch_context.launch_args.insert(0, ["open", "-a"])
|
||||
|
|
@ -23,18 +23,32 @@ def add_implementation_envs(env, _app):
|
|||
env["PYTHONPATH"] = os.pathsep.join(python_path_parts)
|
||||
|
||||
# Modify Blender user scripts path
|
||||
previous_user_scripts = set()
|
||||
# Implementation path is added to set for easier paths check inside loops
|
||||
# - will be removed at the end
|
||||
previous_user_scripts.add(implementation_user_script_path)
|
||||
|
||||
openpype_blender_user_scripts = (
|
||||
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
|
||||
)
|
||||
for path in openpype_blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
|
||||
previous_user_scripts = []
|
||||
for path in blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
path = os.path.normpath(path)
|
||||
if path != implementation_user_script_path:
|
||||
previous_user_scripts.append(path)
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
# Remove implementation path from user script paths as is set to
|
||||
# `BLENDER_USER_SCRIPTS`
|
||||
previous_user_scripts.remove(implementation_user_script_path)
|
||||
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
|
||||
|
||||
# Set custom user scripts env
|
||||
env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join(
|
||||
previous_user_scripts
|
||||
)
|
||||
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
|
||||
|
||||
# Define Qt binding if not defined
|
||||
if not env.get("QT_PREFERRED_BINDING"):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import traceback
|
|||
|
||||
import bpy
|
||||
|
||||
from .lib import append_user_scripts
|
||||
|
||||
from avalon import api as avalon
|
||||
from pyblish import api as pyblish
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ def install():
|
|||
pyblish.register_plugin_path(str(PUBLISH_PATH))
|
||||
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
|
||||
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
|
||||
|
||||
append_user_scripts()
|
||||
avalon.on("new", on_new)
|
||||
avalon.on("open", on_open)
|
||||
|
||||
|
|
|
|||
127
openpype/hosts/blender/api/lib.py
Normal file
127
openpype/hosts/blender/api/lib.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import os
|
||||
import traceback
|
||||
import importlib
|
||||
|
||||
import bpy
|
||||
import addon_utils
|
||||
|
||||
|
||||
def load_scripts(paths):
|
||||
"""Copy of `load_scripts` from Blender's implementation.
|
||||
|
||||
It is possible that whis function will be changed in future and usage will
|
||||
be based on Blender version.
|
||||
"""
|
||||
import bpy_types
|
||||
|
||||
loaded_modules = set()
|
||||
|
||||
previous_classes = [
|
||||
cls
|
||||
for cls in bpy.types.bpy_struct.__subclasses__()
|
||||
]
|
||||
|
||||
def register_module_call(mod):
|
||||
register = getattr(mod, "register", None)
|
||||
if register:
|
||||
try:
|
||||
register()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("\nWarning! '%s' has no register function, "
|
||||
"this is now a requirement for registerable scripts" %
|
||||
mod.__file__)
|
||||
|
||||
def unregister_module_call(mod):
|
||||
unregister = getattr(mod, "unregister", None)
|
||||
if unregister:
|
||||
try:
|
||||
unregister()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def test_reload(mod):
|
||||
# reloading this causes internal errors
|
||||
# because the classes from this module are stored internally
|
||||
# possibly to refresh internal references too but for now, best not to.
|
||||
if mod == bpy_types:
|
||||
return mod
|
||||
|
||||
try:
|
||||
return importlib.reload(mod)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def test_register(mod):
|
||||
if mod:
|
||||
register_module_call(mod)
|
||||
bpy.utils._global_loaded_modules.append(mod.__name__)
|
||||
|
||||
from bpy_restrict_state import RestrictBlend
|
||||
|
||||
with RestrictBlend():
|
||||
for base_path in paths:
|
||||
for path_subdir in bpy.utils._script_module_dirs:
|
||||
path = os.path.join(base_path, path_subdir)
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
|
||||
bpy.utils._sys_path_ensure_prepend(path)
|
||||
|
||||
# Only add to 'sys.modules' unless this is 'startup'.
|
||||
if path_subdir != "startup":
|
||||
continue
|
||||
for mod in bpy.utils.modules_from_path(path, loaded_modules):
|
||||
test_register(mod)
|
||||
|
||||
addons_paths = []
|
||||
for base_path in paths:
|
||||
addons_path = os.path.join(base_path, "addons")
|
||||
if not os.path.exists(addons_path):
|
||||
continue
|
||||
addons_paths.append(addons_path)
|
||||
addons_module_path = os.path.join(addons_path, "modules")
|
||||
if os.path.exists(addons_module_path):
|
||||
bpy.utils._sys_path_ensure_prepend(addons_module_path)
|
||||
|
||||
if addons_paths:
|
||||
# Fake addons
|
||||
origin_paths = addon_utils.paths
|
||||
|
||||
def new_paths():
|
||||
paths = origin_paths() + addons_paths
|
||||
return paths
|
||||
|
||||
addon_utils.paths = new_paths
|
||||
addon_utils.modules_refresh()
|
||||
|
||||
# load template (if set)
|
||||
if any(bpy.utils.app_template_paths()):
|
||||
import bl_app_template_utils
|
||||
bl_app_template_utils.reset(reload_scripts=False)
|
||||
del bl_app_template_utils
|
||||
|
||||
for cls in bpy.types.bpy_struct.__subclasses__():
|
||||
if cls in previous_classes:
|
||||
continue
|
||||
if not getattr(cls, "is_registered", False):
|
||||
continue
|
||||
for subcls in cls.__subclasses__():
|
||||
if not subcls.is_registered:
|
||||
print(
|
||||
"Warning, unregistered class: %s(%s)" %
|
||||
(subcls.__name__, cls.__name__)
|
||||
)
|
||||
|
||||
|
||||
def append_user_scripts():
|
||||
user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS")
|
||||
if not user_scripts:
|
||||
return
|
||||
|
||||
try:
|
||||
load_scripts(user_scripts.split(os.pathsep))
|
||||
except Exception:
|
||||
print("Couldn't load user scripts \"{}\"".format(user_scripts))
|
||||
traceback.print_exc()
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import bpy
|
||||
|
||||
from avalon import api, blender
|
||||
import openpype.hosts.blender.api.plugin
|
||||
|
||||
|
||||
class CreateSetDress(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""A grouped package of loaded content"""
|
||||
|
||||
name = "setdressMain"
|
||||
label = "Set Dress"
|
||||
family = "setdress"
|
||||
icon = "cubes"
|
||||
defaults = ["Main", "Anim"]
|
||||
|
||||
def process(self):
|
||||
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)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
blender.lib.imprint(collection, self.data)
|
||||
|
||||
return collection
|
||||
|
|
@ -25,9 +25,6 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
setdress_creator_name = "CreateSetDress"
|
||||
|
||||
def _remove(self, objects, obj_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
|
|
@ -293,7 +290,6 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
setdress_creator_name = "CreateSetDress"
|
||||
|
||||
def _remove_objects(self, objects):
|
||||
for obj in list(objects):
|
||||
|
|
@ -383,7 +379,7 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
|
||||
def _process(
|
||||
self, libpath, layout_container, container_name, representation,
|
||||
actions, parent
|
||||
actions, parent_collection
|
||||
):
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
|
@ -392,6 +388,11 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
layout_collection = bpy.data.collections.new(container_name)
|
||||
scene.collection.children.link(layout_collection)
|
||||
|
||||
parent = parent_collection
|
||||
|
||||
if parent is None:
|
||||
parent = scene.collection
|
||||
|
||||
all_loaders = api.discover(api.Loader)
|
||||
|
||||
avalon_container = bpy.data.collections.get(
|
||||
|
|
@ -516,23 +517,9 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
# Create a setdress subset to contain all the animation for all
|
||||
# the rigs in the layout
|
||||
creator_plugin = get_creator_by_name(self.setdress_creator_name)
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"{}\" was not found.".format(
|
||||
self.setdress_creator_name
|
||||
))
|
||||
parent = api.create(
|
||||
creator_plugin,
|
||||
name="animation",
|
||||
asset=api.Session["AVALON_ASSET"],
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": str(context["representation"]["_id"])})
|
||||
|
||||
layout_collection = self._process(
|
||||
libpath, layout_container, container_name,
|
||||
str(context["representation"]["_id"]), None, parent)
|
||||
str(context["representation"]["_id"]), None, None)
|
||||
|
||||
container_metadata["obj_container"] = layout_collection
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,9 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
|
||||
if action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
elif local_obj.animation_data.action is not None:
|
||||
plugin.prepare_data(
|
||||
local_obj.animation_data.action, collection_name)
|
||||
|
||||
# Set link the drivers to the local object
|
||||
if local_obj.data.animation_data:
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import openpype.api
|
||||
import pyblish.api
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class ExtractSetDress(openpype.api.Extractor):
|
||||
"""Extract setdress."""
|
||||
|
||||
label = "Extract SetDress"
|
||||
hosts = ["blender"]
|
||||
families = ["setdress"]
|
||||
optional = True
|
||||
order = pyblish.api.ExtractorOrder + 0.1
|
||||
|
||||
def process(self, instance):
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
json_data = []
|
||||
|
||||
for i in instance.context:
|
||||
collection = i.data.get("name")
|
||||
container = None
|
||||
for obj in bpy.data.collections[collection].objects:
|
||||
if obj.type == "ARMATURE":
|
||||
container_name = obj.get("avalon").get("container_name")
|
||||
container = bpy.data.collections[container_name]
|
||||
if container:
|
||||
json_dict = {
|
||||
"subset": i.data.get("subset"),
|
||||
"container": container.name,
|
||||
}
|
||||
json_dict["instance_name"] = container.get("avalon").get(
|
||||
"instance_name"
|
||||
)
|
||||
json_data.append(json_dict)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
json_representation = {
|
||||
"name": "json",
|
||||
"ext": "json",
|
||||
"files": json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.info(
|
||||
"Extracted instance '{}' to: {}".format(instance.name,
|
||||
json_representation)
|
||||
)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import openpype.api
|
||||
|
||||
|
|
@ -121,6 +122,25 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
pair[1].user_clear()
|
||||
bpy.data.actions.remove(pair[1])
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {}
|
||||
|
||||
collection = instance.data.get("name")
|
||||
container = None
|
||||
for obj in bpy.data.collections[collection].objects:
|
||||
if obj.type == "ARMATURE":
|
||||
container_name = obj.get("avalon").get("container_name")
|
||||
container = bpy.data.collections[container_name]
|
||||
if container:
|
||||
json_dict = {
|
||||
"instance_name": container.get("avalon").get("instance_name")
|
||||
}
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_dict, fp=file, indent=2)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
|
|
@ -130,7 +150,15 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
'files': fbx_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
json_representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
|
|
|
|||
|
|
@ -1059,7 +1059,7 @@ class WorkfileSettings(object):
|
|||
# replace reset resolution from avalon core to pype's
|
||||
self.reset_frame_range_handles()
|
||||
# add colorspace menu item
|
||||
# self.set_colorspace()
|
||||
self.set_colorspace()
|
||||
|
||||
def set_favorites(self):
|
||||
work_dir = os.getenv("AVALON_WORKDIR")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from openpype.hosts.nuke.api.lib import (
|
||||
on_script_load,
|
||||
check_inventory_versions
|
||||
check_inventory_versions,
|
||||
WorkfileSettings
|
||||
)
|
||||
|
||||
import nuke
|
||||
|
|
@ -9,8 +10,14 @@ from openpype.api import Logger
|
|||
log = Logger().get_logger(__name__)
|
||||
|
||||
|
||||
nuke.addOnScriptSave(on_script_load)
|
||||
# fix ffmpeg settings on script
|
||||
nuke.addOnScriptLoad(on_script_load)
|
||||
|
||||
# set checker for last versions on loaded containers
|
||||
nuke.addOnScriptLoad(check_inventory_versions)
|
||||
nuke.addOnScriptSave(check_inventory_versions)
|
||||
|
||||
# # set apply all workfile settings on script load and save
|
||||
nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
|
||||
|
||||
log.info('Automatic syncing of write file knob to script version')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
|||
|
||||
|
||||
def on_instance_toggle(instance, old_value, new_value):
|
||||
# Review may not have real instance in wokrfile metadata
|
||||
if not instance.data.get("uuid"):
|
||||
return
|
||||
|
||||
instance_id = instance.data["uuid"]
|
||||
found_idx = None
|
||||
current_instances = pipeline.list_instances()
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from avalon.tvpaint import pipeline
|
||||
from openpype.hosts.tvpaint.api import plugin
|
||||
|
||||
|
||||
class CreateReview(plugin.Creator):
|
||||
"""Review for global review of all layers."""
|
||||
name = "review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "cube"
|
||||
defaults = ["Main"]
|
||||
|
||||
def process(self):
|
||||
instances = pipeline.list_instances()
|
||||
for instance in instances:
|
||||
if instance["family"] == self.family:
|
||||
self.log.info("Review family is already Created.")
|
||||
return
|
||||
super(CreateReview, self).process()
|
||||
|
|
@ -17,6 +17,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
json.dumps(workfile_instances, indent=4)
|
||||
))
|
||||
|
||||
# Backwards compatibility for workfiles that already have review
|
||||
# instance in metadata.
|
||||
review_instance_exist = False
|
||||
for instance_data in workfile_instances:
|
||||
if instance_data["family"] == "review":
|
||||
review_instance_exist = True
|
||||
break
|
||||
|
||||
# Fake review instance if review was not found in metadata families
|
||||
if not review_instance_exist:
|
||||
workfile_instances.append(
|
||||
self._create_review_instance_data(context)
|
||||
)
|
||||
|
||||
for instance_data in workfile_instances:
|
||||
instance_data["fps"] = context.data["sceneFps"]
|
||||
|
||||
|
|
@ -90,6 +104,16 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
instance, json.dumps(instance.data, indent=4)
|
||||
))
|
||||
|
||||
def _create_review_instance_data(self, context):
|
||||
"""Fake review instance data."""
|
||||
|
||||
return {
|
||||
"family": "review",
|
||||
"asset": context.data["workfile_context"]["asset"],
|
||||
# Dummy subset name
|
||||
"subset": "reviewMain"
|
||||
}
|
||||
|
||||
def create_render_layer_instance(self, context, instance_data):
|
||||
name = instance_data["name"]
|
||||
# Change label
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from avalon import api, pipeline
|
||||
from avalon.unreal import lib
|
||||
|
|
@ -61,10 +62,16 @@ class AnimationFBXLoader(api.Loader):
|
|||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
# If there are no options, the process cannot be automated
|
||||
if options:
|
||||
libpath = self.fname.replace("fbx", "json")
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
instance_name = data.get("instance_name")
|
||||
|
||||
if instance_name:
|
||||
automated = True
|
||||
actor_name = 'PersistentLevel.' + options.get('instance_name')
|
||||
actor_name = 'PersistentLevel.' + instance_name
|
||||
actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
|
||||
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
|
@ -81,16 +88,31 @@ class AnimationFBXLoader(api.Loader):
|
|||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', True)
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION)
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
|
||||
task.options.set_editor_property('import_mesh', False)
|
||||
task.options.set_editor_property('import_animations', True)
|
||||
task.options.set_editor_property('override_full_name', True)
|
||||
|
||||
task.options.skeletal_mesh_import_data.set_editor_property(
|
||||
'import_content_type',
|
||||
unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'animation_length',
|
||||
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
|
||||
)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_meshes_in_bone_hierarchy', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'use_default_sample_rate', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_custom_attribute', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_bone_tracks', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'remove_redundant_keys', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'convert_scene', True)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
import json
|
||||
|
||||
from avalon import api
|
||||
import unreal
|
||||
|
||||
|
||||
class AnimationCollectionLoader(api.Loader):
|
||||
"""Load Unreal SkeletalMesh from FBX"""
|
||||
|
||||
families = ["setdress"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Load Animation Collection"
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
from avalon import api, pipeline
|
||||
from avalon.unreal import lib
|
||||
from avalon.unreal import pipeline as unreal_pipeline
|
||||
import unreal
|
||||
|
||||
# Create directory for asset and avalon container
|
||||
root = "/Game/Avalon/Assets"
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
"{}/{}".format(root, asset), suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
libpath = self.fname
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
all_loaders = api.discover(api.Loader)
|
||||
|
||||
for element in data:
|
||||
reference = element.get('_id')
|
||||
|
||||
loaders = api.loaders_from_representation(all_loaders, reference)
|
||||
loader = None
|
||||
for l in loaders:
|
||||
if l.__name__ == "AnimationFBXLoader":
|
||||
loader = l
|
||||
break
|
||||
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
api.load(
|
||||
loader,
|
||||
reference,
|
||||
namespace=instance_name,
|
||||
options=element
|
||||
)
|
||||
|
||||
# Create Asset Container
|
||||
lib.create_avalon_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": pipeline.AVALON_CONTAINER_ID,
|
||||
"asset": asset,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["_id"],
|
||||
"parent": context["representation"]["parent"],
|
||||
"family": context["representation"]["context"]["family"]
|
||||
}
|
||||
unreal_pipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, representation):
|
||||
from avalon import api, io
|
||||
from avalon.unreal import pipeline
|
||||
|
||||
source_path = api.get_representation_path(representation)
|
||||
|
||||
with open(source_path, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
animation_containers = [
|
||||
i for i in pipeline.ls() if
|
||||
i.get('asset') == container.get('asset') and
|
||||
i.get('family') == 'animation']
|
||||
|
||||
for element in data:
|
||||
new_version = io.find_one({"_id": io.ObjectId(element.get('_id'))})
|
||||
new_version_number = new_version.get('context').get('version')
|
||||
anim_container = None
|
||||
for i in animation_containers:
|
||||
if i.get('container_name') == (element.get('subset') + "_CON"):
|
||||
anim_container = i
|
||||
break
|
||||
if not anim_container:
|
||||
continue
|
||||
|
||||
api.update(anim_container, new_version_number)
|
||||
|
||||
container_path = "{}/{}".format(container["namespace"],
|
||||
container["objectName"])
|
||||
# update metadata
|
||||
pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"])
|
||||
})
|
||||
|
||||
def remove(self, container):
|
||||
unreal.EditorAssetLibrary.delete_directory(container["namespace"])
|
||||
|
|
@ -440,7 +440,20 @@ class EnvironmentTool:
|
|||
|
||||
|
||||
class ApplicationExecutable:
|
||||
"""Representation of executable loaded from settings."""
|
||||
|
||||
def __init__(self, executable):
|
||||
# On MacOS check if exists path to executable when ends with `.app`
|
||||
# - it is common that path will lead to "/Applications/Blender" but
|
||||
# real path is "/Applications/Blender.app"
|
||||
if (
|
||||
platform.system().lower() == "darwin"
|
||||
and not os.path.exists(executable)
|
||||
):
|
||||
_executable = executable + ".app"
|
||||
if os.path.exists(_executable):
|
||||
executable = _executable
|
||||
|
||||
self.executable_path = executable
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -1177,17 +1190,23 @@ def prepare_context_environments(data):
|
|||
|
||||
try:
|
||||
workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
|
||||
if not os.path.exists(workdir):
|
||||
log.debug(
|
||||
"Creating workdir folder: \"{}\"".format(workdir)
|
||||
)
|
||||
os.makedirs(workdir)
|
||||
|
||||
except Exception as exc:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Error in anatomy.format: {}".format(str(exc))
|
||||
)
|
||||
|
||||
if not os.path.exists(workdir):
|
||||
log.debug(
|
||||
"Creating workdir folder: \"{}\"".format(workdir)
|
||||
)
|
||||
try:
|
||||
os.makedirs(workdir)
|
||||
except Exception as exc:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Couldn't create workdir because: {}".format(str(exc))
|
||||
)
|
||||
|
||||
context_env = {
|
||||
"AVALON_PROJECT": project_doc["name"],
|
||||
"AVALON_ASSET": asset_doc["name"],
|
||||
|
|
|
|||
303
openpype/lib/delivery.py
Normal file
303
openpype/lib/delivery.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
"""Functions useful for delivery action or loader"""
|
||||
import os
|
||||
import shutil
|
||||
import clique
|
||||
import collections
|
||||
|
||||
def collect_frames(files):
|
||||
"""
|
||||
Returns dict of source path and its frame, if from sequence
|
||||
|
||||
Uses clique as most precise solution
|
||||
|
||||
Args:
|
||||
files(list): list of source paths
|
||||
Returns:
|
||||
(dict): {'/asset/subset_v001.0001.png': '0001', ....}
|
||||
"""
|
||||
collections, remainder = clique.assemble(files)
|
||||
|
||||
sources_and_frames = {}
|
||||
if collections:
|
||||
for collection in collections:
|
||||
src_head = collection.head
|
||||
src_tail = collection.tail
|
||||
|
||||
for index in collection.indexes:
|
||||
src_frame = collection.format("{padding}") % index
|
||||
src_file_name = "{}{}{}".format(src_head, src_frame,
|
||||
src_tail)
|
||||
sources_and_frames[src_file_name] = src_frame
|
||||
else:
|
||||
sources_and_frames[remainder.pop()] = None
|
||||
|
||||
return sources_and_frames
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
"""Returns formatted string with size in appropriate unit"""
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
|
||||
|
||||
def path_from_represenation(representation, anatomy):
|
||||
from avalon import pipeline # safer importing
|
||||
|
||||
try:
|
||||
template = representation["data"]["template"]
|
||||
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
try:
|
||||
context = representation["context"]
|
||||
context["root"] = anatomy.roots
|
||||
path = pipeline.format_template_with_optional_keys(
|
||||
context, template
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
# Template references unavailable data
|
||||
return None
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
|
||||
def copy_file(src_path, dst_path):
|
||||
"""Hardlink file if possible(to save space), copy if not"""
|
||||
from avalon.vendor import filelink # safer importing
|
||||
|
||||
if os.path.exists(dst_path):
|
||||
return
|
||||
try:
|
||||
filelink.create(
|
||||
src_path,
|
||||
dst_path,
|
||||
filelink.HARDLINK
|
||||
)
|
||||
except OSError:
|
||||
shutil.copyfile(src_path, dst_path)
|
||||
|
||||
|
||||
def get_format_dict(anatomy, location_path):
|
||||
"""Returns replaced root values from user provider value.
|
||||
|
||||
Args:
|
||||
anatomy (Anatomy)
|
||||
location_path (str): user provided value
|
||||
Returns:
|
||||
(dict): prepared for formatting of a template
|
||||
"""
|
||||
format_dict = {}
|
||||
if location_path:
|
||||
location_path = location_path.replace("\\", "/")
|
||||
root_names = anatomy.root_names_from_templates(
|
||||
anatomy.templates["delivery"]
|
||||
)
|
||||
if root_names is None:
|
||||
format_dict["root"] = location_path
|
||||
else:
|
||||
format_dict["root"] = {}
|
||||
for name in root_names:
|
||||
format_dict["root"][name] = location_path
|
||||
return format_dict
|
||||
|
||||
|
||||
def check_destination_path(repre_id,
|
||||
anatomy, anatomy_data,
|
||||
datetime_data, template_name):
|
||||
""" Try to create destination path based on 'template_name'.
|
||||
|
||||
In the case that path cannot be filled, template contains unmatched
|
||||
keys, provide error message to filter out repre later.
|
||||
|
||||
Args:
|
||||
anatomy (Anatomy)
|
||||
anatomy_data (dict): context to fill anatomy
|
||||
datetime_data (dict): values with actual date
|
||||
template_name (str): to pick correct delivery template
|
||||
Returns:
|
||||
(collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"}
|
||||
"""
|
||||
anatomy_data.update(datetime_data)
|
||||
anatomy_filled = anatomy.format_all(anatomy_data)
|
||||
dest_path = anatomy_filled["delivery"][template_name]
|
||||
report_items = collections.defaultdict(list)
|
||||
sub_msg = None
|
||||
if not dest_path.solved:
|
||||
msg = (
|
||||
"Missing keys in Representation's context"
|
||||
" for anatomy template \"{}\"."
|
||||
).format(template_name)
|
||||
|
||||
if dest_path.missing_keys:
|
||||
keys = ", ".join(dest_path.missing_keys)
|
||||
sub_msg = (
|
||||
"Representation: {}<br>- Missing keys: \"{}\"<br>"
|
||||
).format(repre_id, keys)
|
||||
|
||||
if dest_path.invalid_types:
|
||||
items = []
|
||||
for key, value in dest_path.invalid_types.items():
|
||||
items.append("\"{}\" {}".format(key, str(value)))
|
||||
|
||||
keys = ", ".join(items)
|
||||
sub_msg = (
|
||||
"Representation: {}<br>"
|
||||
"- Invalid value DataType: \"{}\"<br>"
|
||||
).format(repre_id, keys)
|
||||
|
||||
report_items[msg].append(sub_msg)
|
||||
|
||||
return report_items
|
||||
|
||||
|
||||
def process_single_file(
|
||||
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
|
||||
report_items, log
|
||||
):
|
||||
"""Copy single file to calculated path based on template
|
||||
|
||||
Args:
|
||||
src_path(str): path of source representation file
|
||||
_repre (dict): full repre, used only in process_sequence, here only
|
||||
as to share same signature
|
||||
anatomy (Anatomy)
|
||||
template_name (string): user selected delivery template name
|
||||
anatomy_data (dict): data from repre to fill anatomy with
|
||||
format_dict (dict): root dictionary with names and values
|
||||
report_items (collections.defaultdict): to return error messages
|
||||
log (Logger): for log printing
|
||||
Returns:
|
||||
(collections.defaultdict , int)
|
||||
"""
|
||||
if not os.path.exists(src_path):
|
||||
msg = "{} doesn't exist for {}".format(src_path,
|
||||
repre["_id"])
|
||||
report_items["Source file was not found"].append(msg)
|
||||
return report_items, 0
|
||||
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][template_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][template_name]
|
||||
|
||||
# context.representation could be .psd
|
||||
delivery_path = delivery_path.replace("..", ".")
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
if not os.path.exists(delivery_folder):
|
||||
os.makedirs(delivery_folder)
|
||||
|
||||
log.debug("Copying single: {} -> {}".format(src_path, delivery_path))
|
||||
copy_file(src_path, delivery_path)
|
||||
|
||||
return report_items, 1
|
||||
|
||||
|
||||
def process_sequence(
|
||||
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
|
||||
report_items, log
|
||||
):
|
||||
""" For Pype2(mainly - works in 3 too) where representation might not
|
||||
contain files.
|
||||
|
||||
Uses listing physical files (not 'files' on repre as a)might not be
|
||||
present, b)might not be reliable for representation and copying them.
|
||||
|
||||
TODO Should be refactored when files are sufficient to drive all
|
||||
representations.
|
||||
|
||||
Args:
|
||||
src_path(str): path of source representation file
|
||||
repre (dict): full representation
|
||||
anatomy (Anatomy)
|
||||
template_name (string): user selected delivery template name
|
||||
anatomy_data (dict): data from repre to fill anatomy with
|
||||
format_dict (dict): root dictionary with names and values
|
||||
report_items (collections.defaultdict): to return error messages
|
||||
log (Logger): for log printing
|
||||
Returns:
|
||||
(collections.defaultdict , int)
|
||||
"""
|
||||
if not os.path.exists(src_path):
|
||||
msg = "{} doesn't exist for {}".format(src_path,
|
||||
repre["_id"])
|
||||
report_items["Source file was not found"].append(msg)
|
||||
return report_items, 0
|
||||
|
||||
dir_path, file_name = os.path.split(str(src_path))
|
||||
|
||||
context = repre["context"]
|
||||
ext = context.get("ext", context.get("representation"))
|
||||
|
||||
if not ext:
|
||||
msg = "Source extension not found, cannot find collection"
|
||||
report_items[msg].append(src_path)
|
||||
log.warning("{} <{}>".format(msg, context))
|
||||
return report_items, 0
|
||||
|
||||
ext = "." + ext
|
||||
# context.representation could be .psd
|
||||
ext = ext.replace("..", ".")
|
||||
|
||||
src_collections, remainder = clique.assemble(os.listdir(dir_path))
|
||||
src_collection = None
|
||||
for col in src_collections:
|
||||
if col.tail != ext:
|
||||
continue
|
||||
|
||||
src_collection = col
|
||||
break
|
||||
|
||||
if src_collection is None:
|
||||
msg = "Source collection of files was not found"
|
||||
report_items[msg].append(src_path)
|
||||
log.warning("{} <{}>".format(msg, src_path))
|
||||
return report_items, 0
|
||||
|
||||
frame_indicator = "@####@"
|
||||
|
||||
anatomy_data["frame"] = frame_indicator
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][template_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][template_name]
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
dst_head, dst_tail = delivery_path.split(frame_indicator)
|
||||
dst_padding = src_collection.padding
|
||||
dst_collection = clique.Collection(
|
||||
head=dst_head,
|
||||
tail=dst_tail,
|
||||
padding=dst_padding
|
||||
)
|
||||
|
||||
if not os.path.exists(delivery_folder):
|
||||
os.makedirs(delivery_folder)
|
||||
|
||||
src_head = src_collection.head
|
||||
src_tail = src_collection.tail
|
||||
uploaded = 0
|
||||
for index in src_collection.indexes:
|
||||
src_padding = src_collection.format("{padding}") % index
|
||||
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
|
||||
src = os.path.normpath(
|
||||
os.path.join(dir_path, src_file_name)
|
||||
)
|
||||
|
||||
dst_padding = dst_collection.format("{padding}") % index
|
||||
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
|
||||
log.debug("Copying single: {} -> {}".format(src, dst))
|
||||
copy_file(src, dst)
|
||||
uploaded += 1
|
||||
|
||||
return report_items, uploaded
|
||||
|
|
@ -1101,9 +1101,6 @@ class SyncToAvalonEvent(BaseEvent):
|
|||
# Parents, Hierarchy
|
||||
ent_path_items = [ent["name"] for ent in ftrack_ent["link"]]
|
||||
parents = ent_path_items[1:len(ent_path_items)-1:]
|
||||
hierarchy = ""
|
||||
if len(parents) > 0:
|
||||
hierarchy = os.path.sep.join(parents)
|
||||
|
||||
# TODO logging
|
||||
self.log.debug(
|
||||
|
|
@ -1132,7 +1129,6 @@ class SyncToAvalonEvent(BaseEvent):
|
|||
"ftrackId": ftrack_ent["id"],
|
||||
"entityType": ftrack_ent.entity_type,
|
||||
"parents": parents,
|
||||
"hierarchy": hierarchy,
|
||||
"tasks": {},
|
||||
"visualParent": vis_par
|
||||
}
|
||||
|
|
@ -1975,14 +1971,9 @@ class SyncToAvalonEvent(BaseEvent):
|
|||
if cur_par == parents:
|
||||
continue
|
||||
|
||||
hierarchy = ""
|
||||
if len(parents) > 0:
|
||||
hierarchy = os.path.sep.join(parents)
|
||||
|
||||
if "data" not in self.updates[mongo_id]:
|
||||
self.updates[mongo_id]["data"] = {}
|
||||
self.updates[mongo_id]["data"]["parents"] = parents
|
||||
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
|
||||
|
||||
# Skip custom attributes if didn't change
|
||||
if not hier_cust_attrs_ids:
|
||||
|
|
|
|||
|
|
@ -11,23 +11,28 @@ from avalon.api import AvalonMongoDB
|
|||
class DeleteAssetSubset(BaseAction):
|
||||
'''Edit meta data action.'''
|
||||
|
||||
#: Action identifier.
|
||||
# Action identifier.
|
||||
identifier = "delete.asset.subset"
|
||||
#: Action label.
|
||||
# Action label.
|
||||
label = "Delete Asset/Subsets"
|
||||
#: Action description.
|
||||
# Action description.
|
||||
description = "Removes from Avalon with all childs and asset from Ftrack"
|
||||
icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")
|
||||
|
||||
settings_key = "delete_asset_subset"
|
||||
#: Db connection
|
||||
dbcon = AvalonMongoDB()
|
||||
# Db connection
|
||||
dbcon = None
|
||||
|
||||
splitter = {"type": "label", "value": "---"}
|
||||
action_data_by_id = {}
|
||||
asset_prefix = "asset:"
|
||||
subset_prefix = "subset:"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dbcon = AvalonMongoDB()
|
||||
|
||||
super(DeleteAssetSubset, self).__init__(*args, **kwargs)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
""" Validation """
|
||||
task_ids = []
|
||||
|
|
@ -446,7 +451,14 @@ class DeleteAssetSubset(BaseAction):
|
|||
if len(assets_to_delete) > 0:
|
||||
map_av_ftrack_id = spec_data["without_ftrack_id"]
|
||||
# Prepare data when deleting whole avalon asset
|
||||
avalon_assets = self.dbcon.find({"type": "asset"})
|
||||
avalon_assets = self.dbcon.find(
|
||||
{"type": "asset"},
|
||||
{
|
||||
"_id": 1,
|
||||
"data.visualParent": 1,
|
||||
"data.ftrackId": 1
|
||||
}
|
||||
)
|
||||
avalon_assets_by_parent = collections.defaultdict(list)
|
||||
for asset in avalon_assets:
|
||||
asset_id = asset["_id"]
|
||||
|
|
@ -537,11 +549,13 @@ class DeleteAssetSubset(BaseAction):
|
|||
ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
|
||||
))
|
||||
|
||||
ftrack_ents_to_delete = (
|
||||
entities_by_link_len = (
|
||||
self._filter_entities_to_delete(ftrack_ids_to_delete, session)
|
||||
)
|
||||
for entity in ftrack_ents_to_delete:
|
||||
session.delete(entity)
|
||||
for link_len in sorted(entities_by_link_len.keys(), reverse=True):
|
||||
for entity in entities_by_link_len[link_len]:
|
||||
session.delete(entity)
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
|
|
@ -600,29 +614,11 @@ class DeleteAssetSubset(BaseAction):
|
|||
joined_ids_to_delete
|
||||
)
|
||||
).all()
|
||||
filtered = to_delete_entities[:]
|
||||
while True:
|
||||
changed = False
|
||||
_filtered = filtered[:]
|
||||
for entity in filtered:
|
||||
entity_id = entity["id"]
|
||||
entities_by_link_len = collections.defaultdict(list)
|
||||
for entity in to_delete_entities:
|
||||
entities_by_link_len[len(entity["link"])].append(entity)
|
||||
|
||||
for _entity in tuple(_filtered):
|
||||
if entity_id == _entity["id"]:
|
||||
continue
|
||||
|
||||
for _link in _entity["link"]:
|
||||
if entity_id == _link["id"] and _entity in _filtered:
|
||||
_filtered.remove(_entity)
|
||||
changed = True
|
||||
break
|
||||
|
||||
filtered = _filtered
|
||||
|
||||
if not changed:
|
||||
break
|
||||
|
||||
return filtered
|
||||
return entities_by_link_len
|
||||
|
||||
def report_handle(self, report_messages, project_name, event):
|
||||
if not report_messages:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import os
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import collections
|
||||
|
||||
import clique
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from avalon import pipeline
|
||||
from avalon.vendor import filelink
|
||||
|
||||
from openpype.api import Anatomy, config
|
||||
from openpype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY
|
||||
from openpype.lib.delivery import (
|
||||
path_from_represenation,
|
||||
get_format_dict,
|
||||
check_destination_path,
|
||||
process_single_file,
|
||||
process_sequence
|
||||
)
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
|
||||
|
|
@ -450,18 +452,7 @@ class Delivery(BaseAction):
|
|||
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
format_dict = {}
|
||||
if location_path:
|
||||
location_path = location_path.replace("\\", "/")
|
||||
root_names = anatomy.root_names_from_templates(
|
||||
anatomy.templates["delivery"]
|
||||
)
|
||||
if root_names is None:
|
||||
format_dict["root"] = location_path
|
||||
else:
|
||||
format_dict["root"] = {}
|
||||
for name in root_names:
|
||||
format_dict["root"][name] = location_path
|
||||
format_dict = get_format_dict(anatomy, location_path)
|
||||
|
||||
datetime_data = config.get_datetime_data()
|
||||
for repre in repres_to_deliver:
|
||||
|
|
@ -471,41 +462,15 @@ class Delivery(BaseAction):
|
|||
debug_msg += " with published path {}.".format(source_path)
|
||||
self.log.debug(debug_msg)
|
||||
|
||||
# Get destination repre path
|
||||
anatomy_data = copy.deepcopy(repre["context"])
|
||||
anatomy_data.update(datetime_data)
|
||||
anatomy_filled = anatomy.format_all(anatomy_data)
|
||||
test_path = anatomy_filled["delivery"][anatomy_name]
|
||||
repre_report_items = check_destination_path(repre["_id"],
|
||||
anatomy,
|
||||
anatomy_data,
|
||||
datetime_data,
|
||||
anatomy_name)
|
||||
|
||||
if not test_path.solved:
|
||||
msg = (
|
||||
"Missing keys in Representation's context"
|
||||
" for anatomy template \"{}\"."
|
||||
).format(anatomy_name)
|
||||
|
||||
if test_path.missing_keys:
|
||||
keys = ", ".join(test_path.missing_keys)
|
||||
sub_msg = (
|
||||
"Representation: {}<br>- Missing keys: \"{}\"<br>"
|
||||
).format(str(repre["_id"]), keys)
|
||||
|
||||
if test_path.invalid_types:
|
||||
items = []
|
||||
for key, value in test_path.invalid_types.items():
|
||||
items.append("\"{}\" {}".format(key, str(value)))
|
||||
|
||||
keys = ", ".join(items)
|
||||
sub_msg = (
|
||||
"Representation: {}<br>"
|
||||
"- Invalid value DataType: \"{}\"<br>"
|
||||
).format(str(repre["_id"]), keys)
|
||||
|
||||
report_items[msg].append(sub_msg)
|
||||
self.log.warning(
|
||||
"{} Representation: \"{}\" Filled: <{}>".format(
|
||||
msg, str(repre["_id"]), str(test_path)
|
||||
)
|
||||
)
|
||||
if repre_report_items:
|
||||
report_items.update(repre_report_items)
|
||||
continue
|
||||
|
||||
# Get source repre path
|
||||
|
|
@ -514,151 +479,28 @@ class Delivery(BaseAction):
|
|||
if frame:
|
||||
repre["context"]["frame"] = len(str(frame)) * "#"
|
||||
|
||||
repre_path = self.path_from_represenation(repre, anatomy)
|
||||
repre_path = path_from_represenation(repre, anatomy)
|
||||
# TODO add backup solution where root of path from component
|
||||
# is repalced with root
|
||||
# is replaced with root
|
||||
args = (
|
||||
repre_path,
|
||||
repre,
|
||||
anatomy,
|
||||
anatomy_name,
|
||||
anatomy_data,
|
||||
format_dict,
|
||||
report_items
|
||||
report_items,
|
||||
self.log
|
||||
)
|
||||
if not frame:
|
||||
self.process_single_file(*args)
|
||||
process_single_file(*args)
|
||||
else:
|
||||
self.process_sequence(*args)
|
||||
process_sequence(*args)
|
||||
|
||||
return self.report(report_items)
|
||||
|
||||
def process_single_file(
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
|
||||
report_items
|
||||
):
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][anatomy_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
if not os.path.exists(delivery_folder):
|
||||
os.makedirs(delivery_folder)
|
||||
|
||||
self.copy_file(repre_path, delivery_path)
|
||||
|
||||
def process_sequence(
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
|
||||
report_items
|
||||
):
|
||||
dir_path, file_name = os.path.split(str(repre_path))
|
||||
|
||||
base_name, ext = os.path.splitext(file_name)
|
||||
file_name_items = None
|
||||
if "#" in base_name:
|
||||
file_name_items = [part for part in base_name.split("#") if part]
|
||||
|
||||
elif "%" in base_name:
|
||||
file_name_items = base_name.split("%")
|
||||
|
||||
if not file_name_items:
|
||||
msg = "Source file was not found"
|
||||
report_items[msg].append(repre_path)
|
||||
self.log.warning("{} <{}>".format(msg, repre_path))
|
||||
return
|
||||
|
||||
src_collections, remainder = clique.assemble(os.listdir(dir_path))
|
||||
src_collection = None
|
||||
for col in src_collections:
|
||||
if col.tail != ext:
|
||||
continue
|
||||
|
||||
# skip if collection don't have same basename
|
||||
if not col.head.startswith(file_name_items[0]):
|
||||
continue
|
||||
|
||||
src_collection = col
|
||||
break
|
||||
|
||||
if src_collection is None:
|
||||
# TODO log error!
|
||||
msg = "Source collection of files was not found"
|
||||
report_items[msg].append(repre_path)
|
||||
self.log.warning("{} <{}>".format(msg, repre_path))
|
||||
return
|
||||
|
||||
frame_indicator = "@####@"
|
||||
|
||||
anatomy_data["frame"] = frame_indicator
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][anatomy_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
dst_head, dst_tail = delivery_path.split(frame_indicator)
|
||||
dst_padding = src_collection.padding
|
||||
dst_collection = clique.Collection(
|
||||
head=dst_head,
|
||||
tail=dst_tail,
|
||||
padding=dst_padding
|
||||
)
|
||||
|
||||
if not os.path.exists(delivery_folder):
|
||||
os.makedirs(delivery_folder)
|
||||
|
||||
src_head = src_collection.head
|
||||
src_tail = src_collection.tail
|
||||
for index in src_collection.indexes:
|
||||
src_padding = src_collection.format("{padding}") % index
|
||||
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
|
||||
src = os.path.normpath(
|
||||
os.path.join(dir_path, src_file_name)
|
||||
)
|
||||
|
||||
dst_padding = dst_collection.format("{padding}") % index
|
||||
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
|
||||
|
||||
self.copy_file(src, dst)
|
||||
|
||||
def path_from_represenation(self, representation, anatomy):
|
||||
try:
|
||||
template = representation["data"]["template"]
|
||||
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
try:
|
||||
context = representation["context"]
|
||||
context["root"] = anatomy.roots
|
||||
path = pipeline.format_template_with_optional_keys(
|
||||
context, template
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
# Template references unavailable data
|
||||
return None
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
def copy_file(self, src_path, dst_path):
|
||||
if os.path.exists(dst_path):
|
||||
return
|
||||
try:
|
||||
filelink.create(
|
||||
src_path,
|
||||
dst_path,
|
||||
filelink.HARDLINK
|
||||
)
|
||||
except OSError:
|
||||
shutil.copyfile(src_path, dst_path)
|
||||
|
||||
def report(self, report_items):
|
||||
"""Returns dict with final status of delivery (succes, fail etc.)."""
|
||||
items = []
|
||||
title = "Delivery report"
|
||||
for msg, _items in report_items.items():
|
||||
|
|
|
|||
|
|
@ -1237,12 +1237,8 @@ class SyncEntitiesFactory:
|
|||
|
||||
ent_path_items = [ent["name"] for ent in entity["link"]]
|
||||
parents = ent_path_items[1:len(ent_path_items) - 1:]
|
||||
hierarchy = ""
|
||||
if len(parents) > 0:
|
||||
hierarchy = os.path.sep.join(parents)
|
||||
|
||||
data["parents"] = parents
|
||||
data["hierarchy"] = hierarchy
|
||||
data["tasks"] = self.entities_dict[id].pop("tasks", {})
|
||||
self.entities_dict[id]["final_entity"]["data"] = data
|
||||
self.entities_dict[id]["final_entity"]["type"] = "asset"
|
||||
|
|
@ -2169,8 +2165,6 @@ class SyncEntitiesFactory:
|
|||
hierarchy = "/".join(parents)
|
||||
self.entities_dict[ftrack_id][
|
||||
"final_entity"]["data"]["parents"] = parents
|
||||
self.entities_dict[ftrack_id][
|
||||
"final_entity"]["data"]["hierarchy"] = hierarchy
|
||||
|
||||
_parents.append(self.entities_dict[ftrack_id]["name"])
|
||||
for child_id in self.entities_dict[ftrack_id]["children"]:
|
||||
|
|
@ -2181,7 +2175,6 @@ class SyncEntitiesFactory:
|
|||
if "data" not in self.updates[mongo_id]:
|
||||
self.updates[mongo_id]["data"] = {}
|
||||
self.updates[mongo_id]["data"]["parents"] = parents
|
||||
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
|
||||
|
||||
def prepare_project_changes(self):
|
||||
ftrack_ent_dict = self.entities_dict[self.ft_project_id]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ class LoginServerHandler(BaseHTTPRequestHandler):
|
|||
self.login_callback = login_callback
|
||||
BaseHTTPRequestHandler.__init__(self, *args, **kw)
|
||||
|
||||
def log_message(self, format_str, *args):
|
||||
"""Override method of BaseHTTPRequestHandler.
|
||||
|
||||
Goal is to use `print` instead of `sys.stderr.write`
|
||||
"""
|
||||
# Change
|
||||
print("%s - - [%s] %s\n" % (
|
||||
self.client_address[0],
|
||||
self.log_date_time_string(),
|
||||
format_str % args
|
||||
))
|
||||
|
||||
def do_GET(self):
|
||||
'''Override to handle requests ourselves.'''
|
||||
parsed_path = parse.urlparse(self.path)
|
||||
|
|
|
|||
318
openpype/plugins/load/delivery.py
Normal file
318
openpype/plugins/load/delivery.py
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
from collections import defaultdict
|
||||
import copy
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import api, style
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
from openpype.api import Anatomy, config
|
||||
from openpype import resources
|
||||
|
||||
from openpype.lib.delivery import (
|
||||
sizeof_fmt,
|
||||
path_from_represenation,
|
||||
get_format_dict,
|
||||
check_destination_path,
|
||||
process_single_file,
|
||||
process_sequence,
|
||||
collect_frames
|
||||
)
|
||||
|
||||
|
||||
class Delivery(api.SubsetLoader):
|
||||
"""Export selected versions to folder structure from Template"""
|
||||
|
||||
is_multiple_contexts_compatible = True
|
||||
sequence_splitter = "__sequence_splitter__"
|
||||
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
tool_names = ["library_loader"]
|
||||
|
||||
label = "Deliver Versions"
|
||||
order = 35
|
||||
icon = "upload"
|
||||
color = "#d8d8d8"
|
||||
|
||||
def message(self, text):
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setText(text)
|
||||
msgBox.setStyleSheet(style.load_stylesheet())
|
||||
msgBox.setWindowFlags(
|
||||
msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint
|
||||
)
|
||||
msgBox.exec_()
|
||||
|
||||
def load(self, contexts, name=None, namespace=None, options=None):
|
||||
try:
|
||||
dialog = DeliveryOptionsDialog(contexts, self.log)
|
||||
dialog.exec_()
|
||||
except Exception:
|
||||
self.log.error("Failed to deliver versions.", exc_info=True)
|
||||
|
||||
|
||||
class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select template where to deliver selected representations."""
|
||||
|
||||
def __init__(self, contexts, log=None, parent=None):
|
||||
super(DeliveryOptionsDialog, self).__init__(parent=parent)
|
||||
|
||||
project = contexts[0]["project"]["name"]
|
||||
self.anatomy = Anatomy(project)
|
||||
self._representations = None
|
||||
self.log = log
|
||||
self.currently_uploaded = 0
|
||||
|
||||
self.dbcon = AvalonMongoDB()
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project
|
||||
self.dbcon.install()
|
||||
|
||||
self._set_representations(contexts)
|
||||
|
||||
self.setWindowTitle("OpenPype - Deliver versions")
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
dropdown = QtWidgets.QComboBox()
|
||||
self.templates = self._get_templates(self.anatomy)
|
||||
for name, _ in self.templates.items():
|
||||
dropdown.addItem(name)
|
||||
|
||||
template_label = QtWidgets.QLabel()
|
||||
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
root_line_edit = QtWidgets.QLineEdit()
|
||||
|
||||
repre_checkboxes_layout = QtWidgets.QFormLayout()
|
||||
repre_checkboxes_layout.setContentsMargins(10, 5, 5, 10)
|
||||
|
||||
self._representation_checkboxes = {}
|
||||
for repre in self._get_representation_names():
|
||||
checkbox = QtWidgets.QCheckBox()
|
||||
checkbox.setChecked(False)
|
||||
self._representation_checkboxes[repre] = checkbox
|
||||
|
||||
checkbox.stateChanged.connect(self._update_selected_label)
|
||||
repre_checkboxes_layout.addRow(repre, checkbox)
|
||||
|
||||
selected_label = QtWidgets.QLabel()
|
||||
|
||||
input_widget = QtWidgets.QWidget(self)
|
||||
input_layout = QtWidgets.QFormLayout(input_widget)
|
||||
input_layout.setContentsMargins(10, 15, 5, 5)
|
||||
|
||||
input_layout.addRow("Selected representations", selected_label)
|
||||
input_layout.addRow("Delivery template", dropdown)
|
||||
input_layout.addRow("Template value", template_label)
|
||||
input_layout.addRow("Root", root_line_edit)
|
||||
input_layout.addRow("Representations", repre_checkboxes_layout)
|
||||
|
||||
btn_delivery = QtWidgets.QPushButton("Deliver")
|
||||
btn_delivery.setEnabled(bool(dropdown.currentText()))
|
||||
|
||||
progress_bar = QtWidgets.QProgressBar(self)
|
||||
progress_bar.setMinimum = 0
|
||||
progress_bar.setMaximum = 100
|
||||
progress_bar.setVisible(False)
|
||||
|
||||
text_area = QtWidgets.QTextEdit()
|
||||
text_area.setReadOnly(True)
|
||||
text_area.setVisible(False)
|
||||
text_area.setMinimumHeight(100)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
layout.addWidget(input_widget)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(btn_delivery)
|
||||
layout.addWidget(progress_bar)
|
||||
layout.addWidget(text_area)
|
||||
|
||||
self.selected_label = selected_label
|
||||
self.template_label = template_label
|
||||
self.dropdown = dropdown
|
||||
self.root_line_edit = root_line_edit
|
||||
self.progress_bar = progress_bar
|
||||
self.text_area = text_area
|
||||
self.btn_delivery = btn_delivery
|
||||
|
||||
self.files_selected, self.size_selected = \
|
||||
self._get_counts(self._get_selected_repres())
|
||||
|
||||
self._update_selected_label()
|
||||
self._update_template_value()
|
||||
|
||||
btn_delivery.clicked.connect(self.deliver)
|
||||
dropdown.currentIndexChanged.connect(self._update_template_value)
|
||||
|
||||
def deliver(self):
|
||||
"""Main method to loop through all selected representations"""
|
||||
self.progress_bar.setVisible(True)
|
||||
self.btn_delivery.setEnabled(False)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
report_items = defaultdict(list)
|
||||
|
||||
selected_repres = self._get_selected_repres()
|
||||
|
||||
datetime_data = config.get_datetime_data()
|
||||
template_name = self.dropdown.currentText()
|
||||
format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())
|
||||
for repre in self._representations:
|
||||
if repre["name"] not in selected_repres:
|
||||
continue
|
||||
|
||||
repre_path = path_from_represenation(repre, self.anatomy)
|
||||
|
||||
anatomy_data = copy.deepcopy(repre["context"])
|
||||
new_report_items = check_destination_path(str(repre["_id"]),
|
||||
self.anatomy,
|
||||
anatomy_data,
|
||||
datetime_data,
|
||||
template_name)
|
||||
|
||||
report_items.update(new_report_items)
|
||||
if new_report_items:
|
||||
continue
|
||||
|
||||
args = [
|
||||
repre_path,
|
||||
repre,
|
||||
self.anatomy,
|
||||
template_name,
|
||||
anatomy_data,
|
||||
format_dict,
|
||||
report_items,
|
||||
self.log
|
||||
]
|
||||
|
||||
if repre.get("files"):
|
||||
src_paths = []
|
||||
for repre_file in repre["files"]:
|
||||
src_path = self.anatomy.fill_root(repre_file["path"])
|
||||
src_paths.append(src_path)
|
||||
sources_and_frames = collect_frames(src_paths)
|
||||
|
||||
for src_path, frame in sources_and_frames.items():
|
||||
args[0] = src_path
|
||||
if frame:
|
||||
anatomy_data["frame"] = frame
|
||||
new_report_items, uploaded = process_single_file(*args)
|
||||
report_items.update(new_report_items)
|
||||
self._update_progress(uploaded)
|
||||
else: # fallback for Pype2 and representations without files
|
||||
frame = repre['context'].get('frame')
|
||||
if frame:
|
||||
repre["context"]["frame"] = len(str(frame)) * "#"
|
||||
|
||||
if not frame:
|
||||
new_report_items, uploaded = process_single_file(*args)
|
||||
else:
|
||||
new_report_items, uploaded = process_sequence(*args)
|
||||
report_items.update(new_report_items)
|
||||
self._update_progress(uploaded)
|
||||
|
||||
self.text_area.setText(self._format_report(report_items))
|
||||
self.text_area.setVisible(True)
|
||||
|
||||
def _get_representation_names(self):
|
||||
"""Get set of representation names for checkbox filtering."""
|
||||
return set([repre["name"] for repre in self._representations])
|
||||
|
||||
def _get_templates(self, anatomy):
|
||||
"""Adds list of delivery templates from Anatomy to dropdown."""
|
||||
templates = {}
|
||||
for template_name, value in anatomy.templates["delivery"].items():
|
||||
if not isinstance(value, str) or not value.startswith('{root'):
|
||||
continue
|
||||
|
||||
templates[template_name] = value
|
||||
|
||||
return templates
|
||||
|
||||
def _set_representations(self, contexts):
|
||||
version_ids = [context["version"]["_id"] for context in contexts]
|
||||
|
||||
repres = list(self.dbcon.find({
|
||||
"type": "representation",
|
||||
"parent": {"$in": version_ids}
|
||||
}))
|
||||
|
||||
self._representations = repres
|
||||
|
||||
def _get_counts(self, selected_repres=None):
|
||||
"""Returns tuple of number of selected files and their size."""
|
||||
files_selected = 0
|
||||
size_selected = 0
|
||||
for repre in self._representations:
|
||||
if repre["name"] in selected_repres:
|
||||
files = repre.get("files", [])
|
||||
if not files: # for repre without files, cannot divide by 0
|
||||
files_selected += 1
|
||||
size_selected += 0
|
||||
else:
|
||||
for repre_file in files:
|
||||
files_selected += 1
|
||||
size_selected += repre_file["size"]
|
||||
|
||||
return files_selected, size_selected
|
||||
|
||||
def _prepare_label(self):
|
||||
"""Provides text with no of selected files and their size."""
|
||||
label = "{} files, size {}".format(self.files_selected,
|
||||
sizeof_fmt(self.size_selected))
|
||||
return label
|
||||
|
||||
def _get_selected_repres(self):
|
||||
"""Returns list of representation names filtered from checkboxes."""
|
||||
selected_repres = []
|
||||
for repre_name, chckbox in self._representation_checkboxes.items():
|
||||
if chckbox.isChecked():
|
||||
selected_repres.append(repre_name)
|
||||
|
||||
return selected_repres
|
||||
|
||||
def _update_selected_label(self):
|
||||
"""Updates label with list of number of selected files."""
|
||||
selected_repres = self._get_selected_repres()
|
||||
self.files_selected, self.size_selected = \
|
||||
self._get_counts(selected_repres)
|
||||
self.selected_label.setText(self._prepare_label())
|
||||
|
||||
def _update_template_value(self, _index=None):
|
||||
"""Sets template value to label after selection in dropdown."""
|
||||
name = self.dropdown.currentText()
|
||||
template_value = self.templates.get(name)
|
||||
if template_value:
|
||||
self.btn_delivery.setEnabled(True)
|
||||
self.template_label.setText(template_value)
|
||||
|
||||
def _update_progress(self, uploaded):
|
||||
"""Update progress bar after each repre copied."""
|
||||
self.currently_uploaded += uploaded
|
||||
|
||||
ratio = self.currently_uploaded / self.files_selected
|
||||
self.progress_bar.setValue(ratio * self.progress_bar.maximum())
|
||||
|
||||
def _format_report(self, report_items):
|
||||
"""Format final result and error details as html."""
|
||||
msg = "Delivery finished"
|
||||
if not report_items:
|
||||
msg += " successfully"
|
||||
else:
|
||||
msg += " with errors"
|
||||
txt = "<h2>{}</h2>".format(msg)
|
||||
for header, data in report_items.items():
|
||||
txt += "<h3>{}</h3>".format(header)
|
||||
for item in data:
|
||||
txt += "{}<br>".format(item)
|
||||
|
||||
return txt
|
||||
|
|
@ -56,14 +56,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
audio_inputs.insert(0, empty)
|
||||
|
||||
# create cmd
|
||||
cmd = self.ffmpeg_path + " "
|
||||
cmd = '"{}"'.format(self.ffmpeg_path) + " "
|
||||
cmd += self.create_cmd(audio_inputs)
|
||||
cmd += audio_temp_fpath
|
||||
cmd += "\"{}\"".format(audio_temp_fpath)
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(cmd))
|
||||
openpype.api.run_subprocess(
|
||||
cmd, shell=True, logger=self.log
|
||||
cmd, logger=self.log
|
||||
)
|
||||
|
||||
# remove empty
|
||||
|
|
@ -100,17 +100,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
audio_fpath = self.create_temp_file(name)
|
||||
|
||||
cmd = " ".join([
|
||||
self.ffmpeg_path,
|
||||
'"{}"'.format(self.ffmpeg_path),
|
||||
"-ss {}".format(start_sec),
|
||||
"-t {}".format(duration_sec),
|
||||
"-i {}".format(audio_file),
|
||||
"-i \"{}\"".format(audio_file),
|
||||
audio_fpath
|
||||
])
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(cmd))
|
||||
openpype.api.run_subprocess(
|
||||
cmd, shell=True, logger=self.log
|
||||
cmd, logger=self.log
|
||||
)
|
||||
else:
|
||||
audio_fpath = recycling_file.pop()
|
||||
|
|
@ -221,11 +221,11 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
|
||||
# create empty cmd
|
||||
cmd = " ".join([
|
||||
self.ffmpeg_path,
|
||||
'"{}"'.format(self.ffmpeg_path),
|
||||
"-f lavfi",
|
||||
"-i anullsrc=channel_layout=stereo:sample_rate=48000",
|
||||
"-t {}".format(max_duration_sec),
|
||||
empty_fpath
|
||||
"\"{}\"".format(empty_fpath)
|
||||
])
|
||||
|
||||
# generate empty with ffmpeg
|
||||
|
|
@ -233,7 +233,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
self.log.debug("Executing: {}".format(cmd))
|
||||
|
||||
openpype.api.run_subprocess(
|
||||
cmd, shell=True, logger=self.log
|
||||
cmd, logger=self.log
|
||||
)
|
||||
|
||||
# return dict with output
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"stagingDir": self.staging_dir,
|
||||
"tags": ["review", "ftrackreview", "delete"]
|
||||
"tags": ["review", "delete"]
|
||||
}
|
||||
|
||||
collection = clique.Collection(
|
||||
|
|
@ -313,7 +313,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
out_frame_start += end_offset
|
||||
|
||||
# start command list
|
||||
command = [ffmpeg_path]
|
||||
command = ['"{}"'.format(ffmpeg_path)]
|
||||
|
||||
if sequence:
|
||||
input_dir, collection = sequence
|
||||
|
|
@ -326,7 +326,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
# form command for rendering gap files
|
||||
command.extend([
|
||||
"-start_number {}".format(in_frame_start),
|
||||
"-i {}".format(input_path)
|
||||
"-i \"{}\"".format(input_path)
|
||||
])
|
||||
|
||||
elif video:
|
||||
|
|
@ -341,7 +341,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
command.extend([
|
||||
"-ss {}".format(sec_start),
|
||||
"-t {}".format(sec_duration),
|
||||
"-i {}".format(video_path)
|
||||
"-i \"{}\"".format(video_path)
|
||||
])
|
||||
|
||||
elif gap:
|
||||
|
|
@ -360,11 +360,13 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
# add output attributes
|
||||
command.extend([
|
||||
"-start_number {}".format(out_frame_start),
|
||||
output_path
|
||||
"\"{}\"".format(output_path)
|
||||
])
|
||||
# execute
|
||||
self.log.debug("Executing: {}".format(" ".join(command)))
|
||||
output = openpype.api.run_subprocess(" ".join(command), shell=True)
|
||||
output = openpype.api.run_subprocess(
|
||||
" ".join(command), logger=self.log
|
||||
)
|
||||
self.log.debug("Output: {}".format(output))
|
||||
|
||||
def _generate_used_frames(self, duration, end_offset=None):
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
video_exts = ["mov", "mp4"]
|
||||
supported_exts = image_exts + video_exts
|
||||
|
||||
alpha_exts = ["exr", "png", "dpx"]
|
||||
|
||||
# FFmpeg tools paths
|
||||
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
|
||||
|
||||
|
|
@ -296,6 +298,13 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
):
|
||||
with_audio = False
|
||||
|
||||
input_is_sequence = self.input_is_sequence(repre)
|
||||
input_allow_bg = False
|
||||
if input_is_sequence and repre["files"]:
|
||||
ext = os.path.splitext(repre["files"][0])[1].replace(".", "")
|
||||
if ext in self.alpha_exts:
|
||||
input_allow_bg = True
|
||||
|
||||
return {
|
||||
"fps": float(instance.data["fps"]),
|
||||
"frame_start": frame_start,
|
||||
|
|
@ -310,7 +319,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"resolution_width": instance.data.get("resolutionWidth"),
|
||||
"resolution_height": instance.data.get("resolutionHeight"),
|
||||
"origin_repre": repre,
|
||||
"input_is_sequence": self.input_is_sequence(repre),
|
||||
"input_is_sequence": input_is_sequence,
|
||||
"input_allow_bg": input_allow_bg,
|
||||
"with_audio": with_audio,
|
||||
"without_handles": without_handles,
|
||||
"handles_are_set": handles_are_set
|
||||
|
|
@ -470,6 +480,39 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args)
|
||||
ffmpeg_video_filters.extend(lut_filters)
|
||||
|
||||
bg_alpha = 0
|
||||
bg_color = output_def.get("bg_color")
|
||||
if bg_color:
|
||||
bg_red, bg_green, bg_blue, bg_alpha = bg_color
|
||||
|
||||
if bg_alpha > 0:
|
||||
if not temp_data["input_allow_bg"]:
|
||||
self.log.info((
|
||||
"Output definition has defined BG color input was"
|
||||
" resolved as does not support adding BG."
|
||||
))
|
||||
else:
|
||||
bg_color_hex = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
|
||||
bg_red, bg_green, bg_blue
|
||||
)
|
||||
bg_color_alpha = float(bg_alpha) / 255
|
||||
bg_color_str = "{}@{}".format(bg_color_hex, bg_color_alpha)
|
||||
|
||||
self.log.info("Applying BG color {}".format(bg_color_str))
|
||||
color_args = [
|
||||
"split=2[bg][fg]",
|
||||
"[bg]drawbox=c={}:replace=1:t=fill[bg]".format(
|
||||
bg_color_str
|
||||
),
|
||||
"[bg][fg]overlay=format=auto"
|
||||
]
|
||||
# Prepend bg color change before all video filters
|
||||
# NOTE at the time of creation it is required as video filters
|
||||
# from settings may affect color of BG
|
||||
# e.g. `eq` can remove alpha from input
|
||||
for arg in reversed(color_args):
|
||||
ffmpeg_video_filters.insert(0, arg)
|
||||
|
||||
# Add argument to override output file
|
||||
ffmpeg_output_args.append("-y")
|
||||
|
||||
|
|
@ -547,10 +590,12 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
all_args.append("\"{}\"".format(self.ffmpeg_path))
|
||||
all_args.extend(input_args)
|
||||
if video_filters:
|
||||
all_args.append("-filter:v {}".format(",".join(video_filters)))
|
||||
all_args.append("-filter:v")
|
||||
all_args.append("\"{}\"".format(",".join(video_filters)))
|
||||
|
||||
if audio_filters:
|
||||
all_args.append("-filter:a {}".format(",".join(audio_filters)))
|
||||
all_args.append("-filter:a")
|
||||
all_args.append("\"{}\"".format(",".join(audio_filters)))
|
||||
|
||||
all_args.extend(output_args)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@
|
|||
"ftrackreview"
|
||||
],
|
||||
"ffmpeg_args": {
|
||||
"video_filters": [
|
||||
"eq=gamma=2.2"
|
||||
],
|
||||
"video_filters": [],
|
||||
"audio_filters": [],
|
||||
"input": [],
|
||||
"input": [
|
||||
"-apply_trc gamma22"
|
||||
],
|
||||
"output": [
|
||||
"-pix_fmt yuv420p",
|
||||
"-crf 18",
|
||||
|
|
@ -57,6 +57,12 @@
|
|||
},
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"bg_color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"letter_box": {
|
||||
"enabled": false,
|
||||
"ratio": 0.0,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ from .exceptions import (
|
|||
SchemaError,
|
||||
DefaultsNotDefined,
|
||||
StudioDefaultsNotDefined,
|
||||
BaseInvalidValueType,
|
||||
InvalidValueType,
|
||||
InvalidKeySymbols,
|
||||
SchemaMissingFileInfo,
|
||||
|
|
@ -96,7 +97,7 @@ from .input_entities import (
|
|||
PathInput,
|
||||
RawJsonEntity
|
||||
)
|
||||
|
||||
from .color_entity import ColorEntity
|
||||
from .enum_entity import (
|
||||
BaseEnumEntity,
|
||||
EnumEntity,
|
||||
|
|
@ -115,6 +116,7 @@ from .anatomy_entities import AnatomyEntity
|
|||
__all__ = (
|
||||
"DefaultsNotDefined",
|
||||
"StudioDefaultsNotDefined",
|
||||
"BaseInvalidValueType",
|
||||
"InvalidValueType",
|
||||
"InvalidKeySymbols",
|
||||
"SchemaMissingFileInfo",
|
||||
|
|
@ -146,6 +148,8 @@ __all__ = (
|
|||
"PathInput",
|
||||
"RawJsonEntity",
|
||||
|
||||
"ColorEntity",
|
||||
|
||||
"BaseEnumEntity",
|
||||
"EnumEntity",
|
||||
"AppsEnumEntity",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .lib import (
|
|||
)
|
||||
|
||||
from .exceptions import (
|
||||
BaseInvalidValueType,
|
||||
InvalidValueType,
|
||||
SchemeGroupHierarchyBug,
|
||||
EntitySchemaError
|
||||
|
|
@ -377,7 +378,7 @@ class BaseItemEntity(BaseEntity):
|
|||
|
||||
try:
|
||||
new_value = self.convert_to_valid_type(value)
|
||||
except InvalidValueType:
|
||||
except BaseInvalidValueType:
|
||||
new_value = NOT_SET
|
||||
|
||||
if new_value is not NOT_SET:
|
||||
|
|
|
|||
54
openpype/settings/entities/color_entity.py
Normal file
54
openpype/settings/entities/color_entity.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from .lib import STRING_TYPE
|
||||
from .input_entities import InputEntity
|
||||
from .exceptions import (
|
||||
BaseInvalidValueType,
|
||||
InvalidValueType
|
||||
)
|
||||
|
||||
|
||||
class ColorEntity(InputEntity):
|
||||
schema_types = ["color"]
|
||||
|
||||
def _item_initalization(self):
|
||||
self.valid_value_types = (list, )
|
||||
self.value_on_not_set = [0, 0, 0, 255]
|
||||
|
||||
def convert_to_valid_type(self, value):
|
||||
"""Conversion to valid type.
|
||||
|
||||
Complexity of entity requires to override BaseEntity implementation.
|
||||
"""
|
||||
# Convertion to valid value type `list`
|
||||
if isinstance(value, (set, tuple)):
|
||||
value = list(value)
|
||||
|
||||
# Skip other validations if is not `list`
|
||||
if not isinstance(value, list):
|
||||
raise InvalidValueType(
|
||||
self.valid_value_types, type(value), self.path
|
||||
)
|
||||
|
||||
# Allow list of len 3 (last aplha is set to max)
|
||||
if len(value) == 3:
|
||||
value.append(255)
|
||||
|
||||
if len(value) != 4:
|
||||
reason = "Color entity expect 4 items in list got {}".format(
|
||||
len(value)
|
||||
)
|
||||
raise BaseInvalidValueType(reason, self.path)
|
||||
|
||||
new_value = []
|
||||
for item in value:
|
||||
if not isinstance(item, int):
|
||||
if isinstance(item, (STRING_TYPE, float)):
|
||||
item = int(item)
|
||||
|
||||
is_valid = isinstance(item, int) and -1 < item < 256
|
||||
if not is_valid:
|
||||
reason = (
|
||||
"Color entity expect 4 integers in range 0-255 got {}"
|
||||
).format(value)
|
||||
raise BaseInvalidValueType(reason, self.path)
|
||||
new_value.append(item)
|
||||
return new_value
|
||||
|
|
@ -15,20 +15,22 @@ class StudioDefaultsNotDefined(Exception):
|
|||
super(StudioDefaultsNotDefined, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidValueType(Exception):
|
||||
msg_template = "{}"
|
||||
class BaseInvalidValueType(Exception):
|
||||
def __init__(self, reason, path):
|
||||
msg = "Path \"{}\". {}".format(path, reason)
|
||||
self.msg = msg
|
||||
super(BaseInvalidValueType, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidValueType(BaseInvalidValueType):
|
||||
def __init__(self, valid_types, invalid_type, path):
|
||||
msg = "Path \"{}\". ".format(path)
|
||||
|
||||
joined_types = ", ".join(
|
||||
[str(valid_type) for valid_type in valid_types]
|
||||
)
|
||||
msg += "Got invalid type \"{}\". Expected: {}".format(
|
||||
msg = "Got invalid type \"{}\". Expected: {}".format(
|
||||
invalid_type, joined_types
|
||||
)
|
||||
self.msg = msg
|
||||
super(InvalidValueType, self).__init__(msg)
|
||||
super(InvalidValueType, self).__init__(msg, path)
|
||||
|
||||
|
||||
class RequiredKeyModified(KeyError):
|
||||
|
|
|
|||
|
|
@ -420,6 +420,18 @@
|
|||
}
|
||||
```
|
||||
|
||||
### color
|
||||
- preimplemented entity to store and load color values
|
||||
- entity store and expect list of 4 integers in range 0-255
|
||||
- integers represents rgba [Red, Green, Blue, Alpha]
|
||||
|
||||
```
|
||||
{
|
||||
"type": "color",
|
||||
"key": "bg_color",
|
||||
"label": "Background Color"
|
||||
}
|
||||
```
|
||||
|
||||
## Noninteractive widgets
|
||||
- have nothing to do with data
|
||||
|
|
|
|||
|
|
@ -193,6 +193,15 @@
|
|||
"minimum": 0,
|
||||
"maximum": 100000
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Background color is used only when input have transparency and Alpha is higher than 0."
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"label": "Background color",
|
||||
"key": "bg_color"
|
||||
},
|
||||
{
|
||||
"key": "letter_box",
|
||||
"label": "Letter box",
|
||||
|
|
@ -228,14 +237,9 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_rgba_color",
|
||||
"template_data": [
|
||||
{
|
||||
"label": "Fill Color",
|
||||
"name": "fill_color"
|
||||
}
|
||||
]
|
||||
"type": "color",
|
||||
"label": "Fill Color",
|
||||
"key": "fill_color"
|
||||
},
|
||||
{
|
||||
"key": "line_thickness",
|
||||
|
|
@ -245,14 +249,9 @@
|
|||
"maximum": 1000
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_rgba_color",
|
||||
"template_data": [
|
||||
{
|
||||
"label": "Line Color",
|
||||
"name": "line_color"
|
||||
}
|
||||
]
|
||||
"type": "color",
|
||||
"label": "Line Color",
|
||||
"key": "line_color"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -290,24 +289,14 @@
|
|||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_rgba_color",
|
||||
"template_data": [
|
||||
{
|
||||
"label": "Font Color",
|
||||
"name": "font_color"
|
||||
}
|
||||
]
|
||||
"type": "color",
|
||||
"key": "font_color",
|
||||
"label": "Font Color"
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_rgba_color",
|
||||
"template_data": [
|
||||
{
|
||||
"label": "Background Color",
|
||||
"name": "bg_color"
|
||||
}
|
||||
]
|
||||
"type": "color",
|
||||
"key": "bg_color",
|
||||
"label": "Background Color"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
[
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "{name}",
|
||||
"label": "{label}",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "R",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 255
|
||||
},
|
||||
{
|
||||
"label": "G",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 255
|
||||
},
|
||||
{
|
||||
"label": "B",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 255
|
||||
},
|
||||
{
|
||||
"label": "A",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 255
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -4,6 +4,11 @@
|
|||
"type": "dict",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "color",
|
||||
"label": "Color input",
|
||||
"type": "color"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "schema_template_exaples",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from openpype.settings.entities import (
|
|||
TextEntity,
|
||||
PathInput,
|
||||
RawJsonEntity,
|
||||
ColorEntity,
|
||||
|
||||
DefaultsNotDefined,
|
||||
StudioDefaultsNotDefined,
|
||||
|
|
@ -44,7 +45,7 @@ from .item_widgets import (
|
|||
PathWidget,
|
||||
PathInputWidget
|
||||
)
|
||||
|
||||
from .color_widget import ColorWidget
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
|
||||
|
|
@ -113,6 +114,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
elif isinstance(entity, RawJsonEntity):
|
||||
return RawJsonWidget(*args)
|
||||
|
||||
elif isinstance(entity, ColorEntity):
|
||||
return ColorWidget(*args)
|
||||
|
||||
elif isinstance(entity, BaseEnumEntity):
|
||||
return EnumeratorWidget(*args)
|
||||
|
||||
|
|
|
|||
171
openpype/tools/settings/settings/color_widget.py
Normal file
171
openpype/tools/settings/settings/color_widget.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from .item_widgets import InputWidget
|
||||
|
||||
from openpype.widgets.color_widgets import (
|
||||
ColorPickerWidget,
|
||||
draw_checkerboard_tile
|
||||
)
|
||||
|
||||
|
||||
class ColorWidget(InputWidget):
|
||||
def _add_inputs_to_layout(self):
|
||||
self.input_field = ColorViewer(self.content_widget)
|
||||
|
||||
self.setFocusProxy(self.input_field)
|
||||
|
||||
self.content_layout.addWidget(self.input_field, 1)
|
||||
|
||||
self.input_field.clicked.connect(self._on_click)
|
||||
|
||||
self._dialog = None
|
||||
|
||||
def _on_click(self):
|
||||
if self._dialog:
|
||||
self._dialog.open()
|
||||
return
|
||||
|
||||
dialog = ColorDialog(self.input_field.color(), self)
|
||||
self._dialog = dialog
|
||||
|
||||
dialog.open()
|
||||
dialog.finished.connect(self._on_dialog_finish)
|
||||
|
||||
def _on_dialog_finish(self, *_args):
|
||||
if not self._dialog:
|
||||
return
|
||||
|
||||
color = self._dialog.result()
|
||||
if color is not None:
|
||||
self.input_field.set_color(color)
|
||||
self._on_value_change()
|
||||
|
||||
self._dialog.deleteLater()
|
||||
self._dialog = None
|
||||
|
||||
def _on_entity_change(self):
|
||||
if self.entity.value != self.input_value():
|
||||
self.set_entity_value()
|
||||
|
||||
def set_entity_value(self):
|
||||
self.input_field.set_color(*self.entity.value)
|
||||
|
||||
def input_value(self):
|
||||
color = self.input_field.color()
|
||||
return [color.red(), color.green(), color.blue(), color.alpha()]
|
||||
|
||||
def _on_value_change(self):
|
||||
if self.ignore_input_changes:
|
||||
return
|
||||
|
||||
self.entity.set(self.input_value())
|
||||
|
||||
|
||||
class ColorViewer(QtWidgets.QWidget):
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ColorViewer, self).__init__(parent)
|
||||
|
||||
self.setMinimumSize(10, 10)
|
||||
|
||||
self.actual_pen = QtGui.QPen()
|
||||
self.actual_color = QtGui.QColor()
|
||||
self._checkerboard = None
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit()
|
||||
super(ColorViewer, self).mouseReleaseEvent(event)
|
||||
|
||||
def checkerboard(self):
|
||||
if not self._checkerboard:
|
||||
self._checkerboard = draw_checkerboard_tile(self.height() / 4)
|
||||
return self._checkerboard
|
||||
|
||||
def color(self):
|
||||
return self.actual_color
|
||||
|
||||
def set_color(self, *args):
|
||||
# Create copy of entered color
|
||||
self.actual_color = QtGui.QColor(*args)
|
||||
# Repaint
|
||||
self.update()
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
# Change alpha of current color
|
||||
self.actual_color.setAlpha(alpha)
|
||||
# Repaint
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
rect = event.rect()
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
radius = rect.height() / 2
|
||||
rounded_rect = QtGui.QPainterPath()
|
||||
rounded_rect.addRoundedRect(QtCore.QRectF(rect), radius, radius)
|
||||
painter.setClipPath(rounded_rect)
|
||||
|
||||
pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))
|
||||
pen.setWidth(1)
|
||||
painter.setPen(pen)
|
||||
painter.drawTiledPixmap(rect, self.checkerboard())
|
||||
painter.fillRect(rect, self.actual_color)
|
||||
painter.drawPath(rounded_rect)
|
||||
|
||||
painter.end()
|
||||
|
||||
|
||||
class ColorDialog(QtWidgets.QDialog):
|
||||
def __init__(self, color=None, parent=None):
|
||||
super(ColorDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Color picker dialog")
|
||||
|
||||
picker_widget = ColorPickerWidget(color, self)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("Ok", footer_widget)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(ok_btn)
|
||||
footer_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
layout.addWidget(picker_widget, 1)
|
||||
layout.addWidget(footer_widget, 0)
|
||||
|
||||
ok_btn.clicked.connect(self.on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self.on_cancel_clicked)
|
||||
|
||||
self.picker_widget = picker_widget
|
||||
self.ok_btn = ok_btn
|
||||
self.cancel_btn = cancel_btn
|
||||
|
||||
self._result = None
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ColorDialog, self).showEvent(event)
|
||||
|
||||
btns_width = max(self.ok_btn.width(), self.cancel_btn.width())
|
||||
self.ok_btn.setFixedWidth(btns_width)
|
||||
self.cancel_btn.setFixedWidth(btns_width)
|
||||
|
||||
def on_ok_clicked(self):
|
||||
self._result = self.picker_widget.color()
|
||||
self.close()
|
||||
|
||||
def on_cancel_clicked(self):
|
||||
self._result = None
|
||||
self.close()
|
||||
|
||||
def result(self):
|
||||
return self._result
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.0.0-rc.5"
|
||||
__version__ = "3.0.0-rc.6"
|
||||
|
|
|
|||
14
openpype/widgets/color_widgets/__init__.py
Normal file
14
openpype/widgets/color_widgets/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from .color_picker_widget import (
|
||||
ColorPickerWidget
|
||||
)
|
||||
|
||||
from .color_view import (
|
||||
draw_checkerboard_tile
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ColorPickerWidget",
|
||||
|
||||
"draw_checkerboard_tile"
|
||||
)
|
||||
639
openpype/widgets/color_widgets/color_inputs.py
Normal file
639
openpype/widgets/color_widgets/color_inputs.py
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
import re
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from .color_view import draw_checkerboard_tile
|
||||
|
||||
|
||||
slide_style = """
|
||||
QSlider::groove:horizontal {
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff
|
||||
);
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb
|
||||
);
|
||||
border: 1px solid #777;
|
||||
width: 8px;
|
||||
margin-top: -1px;
|
||||
margin-bottom: -1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd
|
||||
);
|
||||
border: 1px solid #444;ff
|
||||
border-radius: 4px;
|
||||
}"""
|
||||
|
||||
|
||||
class AlphaSlider(QtWidgets.QSlider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AlphaSlider, self).__init__(*args, **kwargs)
|
||||
self._mouse_clicked = False
|
||||
self.setSingleStep(1)
|
||||
self.setMinimum(0)
|
||||
self.setMaximum(255)
|
||||
self.setValue(255)
|
||||
|
||||
self._checkerboard = None
|
||||
|
||||
def checkerboard(self):
|
||||
if self._checkerboard is None:
|
||||
self._checkerboard = draw_checkerboard_tile(
|
||||
3, QtGui.QColor(173, 173, 173), QtGui.QColor(27, 27, 27)
|
||||
)
|
||||
return self._checkerboard
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self._mouse_clicked = True
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._set_value_to_pos(event.pos().x())
|
||||
return event.accept()
|
||||
return super(AlphaSlider, self).mousePressEvent(event)
|
||||
|
||||
def _set_value_to_pos(self, pos_x):
|
||||
value = (
|
||||
self.maximum() - self.minimum()
|
||||
) * pos_x / self.width() + self.minimum()
|
||||
self.setValue(value)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self._mouse_clicked:
|
||||
self._set_value_to_pos(event.pos().x())
|
||||
super(AlphaSlider, self).mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._mouse_clicked = True
|
||||
super(AlphaSlider, self).mouseReleaseEvent(event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
opt = QtWidgets.QStyleOptionSlider()
|
||||
self.initStyleOption(opt)
|
||||
|
||||
painter.fillRect(event.rect(), QtCore.Qt.transparent)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
|
||||
rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_Slider,
|
||||
opt,
|
||||
QtWidgets.QStyle.SC_SliderGroove,
|
||||
self
|
||||
)
|
||||
final_height = 9
|
||||
offset_top = 0
|
||||
if rect.height() > final_height:
|
||||
offset_top = int((rect.height() - final_height) / 2)
|
||||
rect = QtCore.QRect(
|
||||
rect.x(),
|
||||
offset_top,
|
||||
rect.width(),
|
||||
final_height
|
||||
)
|
||||
|
||||
pix_rect = QtCore.QRect(event.rect())
|
||||
pix_rect.setX(rect.x())
|
||||
pix_rect.setWidth(rect.width() - (2 * rect.x()))
|
||||
pix = QtGui.QPixmap(pix_rect.width(), pix_rect.height())
|
||||
pix_painter = QtGui.QPainter(pix)
|
||||
pix_painter.drawTiledPixmap(pix_rect, self.checkerboard())
|
||||
gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight())
|
||||
gradient.setColorAt(0, QtCore.Qt.transparent)
|
||||
gradient.setColorAt(1, QtCore.Qt.white)
|
||||
pix_painter.fillRect(pix_rect, gradient)
|
||||
pix_painter.end()
|
||||
|
||||
brush = QtGui.QBrush(pix)
|
||||
painter.save()
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(brush)
|
||||
ratio = rect.height() / 2
|
||||
painter.drawRoundedRect(rect, ratio, ratio)
|
||||
painter.restore()
|
||||
|
||||
_handle_rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_Slider,
|
||||
opt,
|
||||
QtWidgets.QStyle.SC_SliderHandle,
|
||||
self
|
||||
)
|
||||
|
||||
handle_rect = QtCore.QRect(rect)
|
||||
if offset_top > 1:
|
||||
height = handle_rect.height()
|
||||
handle_rect.setY(handle_rect.y() - 1)
|
||||
handle_rect.setHeight(height + 2)
|
||||
handle_rect.setX(_handle_rect.x())
|
||||
handle_rect.setWidth(handle_rect.height())
|
||||
|
||||
painter.save()
|
||||
|
||||
gradient = QtGui.QRadialGradient()
|
||||
radius = handle_rect.height() / 2
|
||||
center_x = handle_rect.width() / 2 + handle_rect.x()
|
||||
center_y = handle_rect.height()
|
||||
gradient.setCenter(center_x, center_y)
|
||||
gradient.setCenterRadius(radius)
|
||||
gradient.setFocalPoint(center_x, center_y)
|
||||
|
||||
gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127))
|
||||
gradient.setColorAt(1, QtCore.Qt.transparent)
|
||||
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(gradient)
|
||||
painter.drawEllipse(handle_rect)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
class AlphaInputs(QtWidgets.QWidget):
|
||||
alpha_changed = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(AlphaInputs, self).__init__(parent)
|
||||
|
||||
self._block_changes = False
|
||||
self.alpha_value = None
|
||||
|
||||
percent_input = QtWidgets.QDoubleSpinBox(self)
|
||||
percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
percent_input.setMinimum(0)
|
||||
percent_input.setMaximum(100)
|
||||
percent_input.setDecimals(2)
|
||||
|
||||
int_input = QtWidgets.QSpinBox(self)
|
||||
int_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
int_input.setMinimum(0)
|
||||
int_input.setMaximum(255)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(int_input)
|
||||
layout.addWidget(QtWidgets.QLabel("0-255"))
|
||||
layout.addWidget(percent_input)
|
||||
layout.addWidget(QtWidgets.QLabel("%"))
|
||||
|
||||
percent_input.valueChanged.connect(self._on_percent_change)
|
||||
int_input.valueChanged.connect(self._on_int_change)
|
||||
|
||||
self.percent_input = percent_input
|
||||
self.int_input = int_input
|
||||
|
||||
self.set_alpha(255)
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
if alpha == self.alpha_value:
|
||||
return
|
||||
self.alpha_value = alpha
|
||||
|
||||
self.update_alpha()
|
||||
|
||||
def _on_percent_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.alpha_value = int(self.percent_input.value() * 255 / 100)
|
||||
self.alpha_changed.emit(self.alpha_value)
|
||||
self.update_alpha()
|
||||
|
||||
def _on_int_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
|
||||
self.alpha_value = self.int_input.value()
|
||||
self.alpha_changed.emit(self.alpha_value)
|
||||
self.update_alpha()
|
||||
|
||||
def update_alpha(self):
|
||||
self._block_changes = True
|
||||
if self.int_input.value() != self.alpha_value:
|
||||
self.int_input.setValue(self.alpha_value)
|
||||
|
||||
percent = round(100 * self.alpha_value / 255, 2)
|
||||
if self.percent_input.value() != percent:
|
||||
self.percent_input.setValue(percent)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class RGBInputs(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, color, parent=None):
|
||||
super(RGBInputs, self).__init__(parent)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
self.color = color
|
||||
|
||||
input_red = QtWidgets.QSpinBox(self)
|
||||
input_green = QtWidgets.QSpinBox(self)
|
||||
input_blue = QtWidgets.QSpinBox(self)
|
||||
|
||||
input_red.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_green.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_blue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
|
||||
input_red.setMinimum(0)
|
||||
input_green.setMinimum(0)
|
||||
input_blue.setMinimum(0)
|
||||
|
||||
input_red.setMaximum(255)
|
||||
input_green.setMaximum(255)
|
||||
input_blue.setMaximum(255)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(input_red, 1)
|
||||
layout.addWidget(input_green, 1)
|
||||
layout.addWidget(input_blue, 1)
|
||||
|
||||
input_red.valueChanged.connect(self._on_red_change)
|
||||
input_green.valueChanged.connect(self._on_green_change)
|
||||
input_blue.valueChanged.connect(self._on_blue_change)
|
||||
|
||||
self.input_red = input_red
|
||||
self.input_green = input_green
|
||||
self.input_blue = input_blue
|
||||
|
||||
def _on_red_change(self, value):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setRed(value)
|
||||
self._on_change()
|
||||
|
||||
def _on_green_change(self, value):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setGreen(value)
|
||||
self._on_change()
|
||||
|
||||
def _on_blue_change(self, value):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setBlue(value)
|
||||
self._on_change()
|
||||
|
||||
def _on_change(self):
|
||||
self.value_changed.emit()
|
||||
|
||||
def color_changed(self):
|
||||
if (
|
||||
self.input_red.value() == self.color.red()
|
||||
and self.input_green.value() == self.color.green()
|
||||
and self.input_blue.value() == self.color.blue()
|
||||
):
|
||||
return
|
||||
|
||||
self._block_changes = True
|
||||
|
||||
self.input_red.setValue(self.color.red())
|
||||
self.input_green.setValue(self.color.green())
|
||||
self.input_blue.setValue(self.color.blue())
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class CMYKInputs(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, color, parent=None):
|
||||
super(CMYKInputs, self).__init__(parent)
|
||||
|
||||
self.color = color
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
input_cyan = QtWidgets.QSpinBox(self)
|
||||
input_magenta = QtWidgets.QSpinBox(self)
|
||||
input_yellow = QtWidgets.QSpinBox(self)
|
||||
input_black = QtWidgets.QSpinBox(self)
|
||||
|
||||
input_cyan.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_magenta.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_yellow.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_black.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
|
||||
input_cyan.setMinimum(0)
|
||||
input_magenta.setMinimum(0)
|
||||
input_yellow.setMinimum(0)
|
||||
input_black.setMinimum(0)
|
||||
|
||||
input_cyan.setMaximum(255)
|
||||
input_magenta.setMaximum(255)
|
||||
input_yellow.setMaximum(255)
|
||||
input_black.setMaximum(255)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(input_cyan, 1)
|
||||
layout.addWidget(input_magenta, 1)
|
||||
layout.addWidget(input_yellow, 1)
|
||||
layout.addWidget(input_black, 1)
|
||||
|
||||
input_cyan.valueChanged.connect(self._on_change)
|
||||
input_magenta.valueChanged.connect(self._on_change)
|
||||
input_yellow.valueChanged.connect(self._on_change)
|
||||
input_black.valueChanged.connect(self._on_change)
|
||||
|
||||
self.input_cyan = input_cyan
|
||||
self.input_magenta = input_magenta
|
||||
self.input_yellow = input_yellow
|
||||
self.input_black = input_black
|
||||
|
||||
def _on_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setCmyk(
|
||||
self.input_cyan.value(),
|
||||
self.input_magenta.value(),
|
||||
self.input_yellow.value(),
|
||||
self.input_black.value()
|
||||
)
|
||||
self.value_changed.emit()
|
||||
|
||||
def color_changed(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
_cur_color = QtGui.QColor()
|
||||
_cur_color.setCmyk(
|
||||
self.input_cyan.value(),
|
||||
self.input_magenta.value(),
|
||||
self.input_yellow.value(),
|
||||
self.input_black.value()
|
||||
)
|
||||
if (
|
||||
_cur_color.red() == self.color.red()
|
||||
and _cur_color.green() == self.color.green()
|
||||
and _cur_color.blue() == self.color.blue()
|
||||
):
|
||||
return
|
||||
|
||||
c, m, y, k, _ = self.color.getCmyk()
|
||||
self._block_changes = True
|
||||
|
||||
self.input_cyan.setValue(c)
|
||||
self.input_magenta.setValue(m)
|
||||
self.input_yellow.setValue(y)
|
||||
self.input_black.setValue(k)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class HEXInputs(QtWidgets.QWidget):
|
||||
hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$")
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, color, parent=None):
|
||||
super(HEXInputs, self).__init__(parent)
|
||||
self.color = color
|
||||
|
||||
input_field = QtWidgets.QLineEdit(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(input_field, 1)
|
||||
|
||||
input_field.textChanged.connect(self._on_change)
|
||||
|
||||
self.input_field = input_field
|
||||
|
||||
def _on_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
input_value = self.input_field.text()
|
||||
# TODO what if does not match?
|
||||
if self.hex_regex.match(input_value):
|
||||
self.color.setNamedColor(input_value)
|
||||
self.value_changed.emit()
|
||||
|
||||
def color_changed(self):
|
||||
input_value = self.input_field.text()
|
||||
if self.hex_regex.match(input_value):
|
||||
_cur_color = QtGui.QColor()
|
||||
_cur_color.setNamedColor(input_value)
|
||||
if (
|
||||
_cur_color.red() == self.color.red()
|
||||
and _cur_color.green() == self.color.green()
|
||||
and _cur_color.blue() == self.color.blue()
|
||||
):
|
||||
return
|
||||
self._block_changes = True
|
||||
|
||||
self.input_field.setText(self.color.name())
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class HSVInputs(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, color, parent=None):
|
||||
super(HSVInputs, self).__init__(parent)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
self.color = color
|
||||
|
||||
input_hue = QtWidgets.QSpinBox(self)
|
||||
input_sat = QtWidgets.QSpinBox(self)
|
||||
input_val = QtWidgets.QSpinBox(self)
|
||||
|
||||
input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_val.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
|
||||
input_hue.setMinimum(0)
|
||||
input_sat.setMinimum(0)
|
||||
input_val.setMinimum(0)
|
||||
|
||||
input_hue.setMaximum(359)
|
||||
input_sat.setMaximum(255)
|
||||
input_val.setMaximum(255)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(input_hue, 1)
|
||||
layout.addWidget(input_sat, 1)
|
||||
layout.addWidget(input_val, 1)
|
||||
|
||||
input_hue.valueChanged.connect(self._on_change)
|
||||
input_sat.valueChanged.connect(self._on_change)
|
||||
input_val.valueChanged.connect(self._on_change)
|
||||
|
||||
self.input_hue = input_hue
|
||||
self.input_sat = input_sat
|
||||
self.input_val = input_val
|
||||
|
||||
def _on_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setHsv(
|
||||
self.input_hue.value(),
|
||||
self.input_sat.value(),
|
||||
self.input_val.value()
|
||||
)
|
||||
self.value_changed.emit()
|
||||
|
||||
def color_changed(self):
|
||||
_cur_color = QtGui.QColor()
|
||||
_cur_color.setHsv(
|
||||
self.input_hue.value(),
|
||||
self.input_sat.value(),
|
||||
self.input_val.value()
|
||||
)
|
||||
if (
|
||||
_cur_color.red() == self.color.red()
|
||||
and _cur_color.green() == self.color.green()
|
||||
and _cur_color.blue() == self.color.blue()
|
||||
):
|
||||
return
|
||||
|
||||
self._block_changes = True
|
||||
h, s, v, _ = self.color.getHsv()
|
||||
|
||||
self.input_hue.setValue(h)
|
||||
self.input_sat.setValue(s)
|
||||
self.input_val.setValue(v)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class HSLInputs(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, color, parent=None):
|
||||
super(HSLInputs, self).__init__(parent)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
self.color = color
|
||||
|
||||
input_hue = QtWidgets.QSpinBox(self)
|
||||
input_sat = QtWidgets.QSpinBox(self)
|
||||
input_light = QtWidgets.QSpinBox(self)
|
||||
|
||||
input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
input_light.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
|
||||
input_hue.setMinimum(0)
|
||||
input_sat.setMinimum(0)
|
||||
input_light.setMinimum(0)
|
||||
|
||||
input_hue.setMaximum(359)
|
||||
input_sat.setMaximum(255)
|
||||
input_light.setMaximum(255)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(input_hue, 1)
|
||||
layout.addWidget(input_sat, 1)
|
||||
layout.addWidget(input_light, 1)
|
||||
|
||||
input_hue.valueChanged.connect(self._on_change)
|
||||
input_sat.valueChanged.connect(self._on_change)
|
||||
input_light.valueChanged.connect(self._on_change)
|
||||
|
||||
self.input_hue = input_hue
|
||||
self.input_sat = input_sat
|
||||
self.input_light = input_light
|
||||
|
||||
def _on_change(self):
|
||||
if self._block_changes:
|
||||
return
|
||||
self.color.setHsl(
|
||||
self.input_hue.value(),
|
||||
self.input_sat.value(),
|
||||
self.input_light.value()
|
||||
)
|
||||
self.value_changed.emit()
|
||||
|
||||
def color_changed(self):
|
||||
_cur_color = QtGui.QColor()
|
||||
_cur_color.setHsl(
|
||||
self.input_hue.value(),
|
||||
self.input_sat.value(),
|
||||
self.input_light.value()
|
||||
)
|
||||
if (
|
||||
_cur_color.red() == self.color.red()
|
||||
and _cur_color.green() == self.color.green()
|
||||
and _cur_color.blue() == self.color.blue()
|
||||
):
|
||||
return
|
||||
|
||||
self._block_changes = True
|
||||
h, s, l, _ = self.color.getHsl()
|
||||
|
||||
self.input_hue.setValue(h)
|
||||
self.input_sat.setValue(s)
|
||||
self.input_light.setValue(l)
|
||||
|
||||
self._block_changes = False
|
||||
|
||||
|
||||
class ColorInputsWidget(QtWidgets.QWidget):
|
||||
color_changed = QtCore.Signal(QtGui.QColor)
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super(ColorInputsWidget, self).__init__(parent)
|
||||
|
||||
color = QtGui.QColor()
|
||||
|
||||
input_fields = []
|
||||
|
||||
if kwargs.get("use_hex", True):
|
||||
input_fields.append(HEXInputs(color, self))
|
||||
|
||||
if kwargs.get("use_rgb", True):
|
||||
input_fields.append(RGBInputs(color, self))
|
||||
|
||||
if kwargs.get("use_hsl", True):
|
||||
input_fields.append(HSLInputs(color, self))
|
||||
|
||||
if kwargs.get("use_hsv", True):
|
||||
input_fields.append(HSVInputs(color, self))
|
||||
|
||||
if kwargs.get("use_cmyk", True):
|
||||
input_fields.append(CMYKInputs(color, self))
|
||||
|
||||
inputs_widget = QtWidgets.QWidget(self)
|
||||
inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)
|
||||
|
||||
for input_field in input_fields:
|
||||
inputs_layout.addWidget(input_field)
|
||||
input_field.value_changed.connect(self._on_value_change)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(inputs_widget, 0)
|
||||
spacer = QtWidgets.QWidget(self)
|
||||
layout.addWidget(spacer, 1)
|
||||
|
||||
self.input_fields = input_fields
|
||||
|
||||
self.color = color
|
||||
|
||||
def set_color(self, color):
|
||||
if (
|
||||
color.red() == self.color.red()
|
||||
and color.green() == self.color.green()
|
||||
and color.blue() == self.color.blue()
|
||||
):
|
||||
return
|
||||
self.color.setRed(color.red())
|
||||
self.color.setGreen(color.green())
|
||||
self.color.setBlue(color.blue())
|
||||
self._on_value_change()
|
||||
|
||||
def _on_value_change(self):
|
||||
for input_field in self.input_fields:
|
||||
input_field.color_changed()
|
||||
self.color_changed.emit(self.color)
|
||||
176
openpype/widgets/color_widgets/color_picker_widget.py
Normal file
176
openpype/widgets/color_widgets/color_picker_widget.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import os
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from .color_triangle import QtColorTriangle
|
||||
from .color_view import ColorViewer
|
||||
from .color_screen_pick import PickScreenColorWidget
|
||||
from .color_inputs import (
|
||||
AlphaSlider,
|
||||
AlphaInputs,
|
||||
HEXInputs,
|
||||
RGBInputs,
|
||||
HSLInputs,
|
||||
HSVInputs
|
||||
)
|
||||
|
||||
|
||||
class ColorPickerWidget(QtWidgets.QWidget):
|
||||
color_changed = QtCore.Signal(QtGui.QColor)
|
||||
|
||||
def __init__(self, color=None, parent=None):
|
||||
super(ColorPickerWidget, self).__init__(parent)
|
||||
|
||||
# Color triangle
|
||||
color_triangle = QtColorTriangle(self)
|
||||
|
||||
alpha_slider_proxy = QtWidgets.QWidget(self)
|
||||
alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy)
|
||||
|
||||
alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy)
|
||||
alpha_slider_layout.setContentsMargins(5, 5, 5, 5)
|
||||
alpha_slider_layout.addWidget(alpha_slider, 1)
|
||||
|
||||
# Eye picked widget
|
||||
pick_widget = PickScreenColorWidget()
|
||||
pick_widget.setMaximumHeight(50)
|
||||
|
||||
# Color pick button
|
||||
btn_pick_color = QtWidgets.QPushButton(self)
|
||||
icon_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"eyedropper.png"
|
||||
)
|
||||
btn_pick_color.setIcon(QtGui.QIcon(icon_path))
|
||||
btn_pick_color.setToolTip("Pick a color")
|
||||
|
||||
# Color preview
|
||||
color_view = ColorViewer(self)
|
||||
color_view.setMaximumHeight(50)
|
||||
|
||||
alpha_inputs = AlphaInputs(self)
|
||||
|
||||
color_inputs_color = QtGui.QColor()
|
||||
col_inputs_by_label = [
|
||||
("HEX", HEXInputs(color_inputs_color, self)),
|
||||
("RGB", RGBInputs(color_inputs_color, self)),
|
||||
("HSL", HSLInputs(color_inputs_color, self)),
|
||||
("HSV", HSVInputs(color_inputs_color, self))
|
||||
]
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
empty_col = 1
|
||||
label_col = empty_col + 1
|
||||
input_col = label_col + 1
|
||||
empty_widget = QtWidgets.QWidget(self)
|
||||
empty_widget.setFixedWidth(10)
|
||||
layout.addWidget(empty_widget, 0, empty_col)
|
||||
|
||||
row = 0
|
||||
layout.addWidget(btn_pick_color, row, label_col)
|
||||
layout.addWidget(color_view, row, input_col)
|
||||
row += 1
|
||||
|
||||
color_input_fields = []
|
||||
for label, input_field in col_inputs_by_label:
|
||||
layout.addWidget(QtWidgets.QLabel(label, self), row, label_col)
|
||||
layout.addWidget(input_field, row, input_col)
|
||||
input_field.value_changed.connect(
|
||||
self._on_color_input_value_change
|
||||
)
|
||||
color_input_fields.append(input_field)
|
||||
row += 1
|
||||
|
||||
layout.addWidget(color_triangle, 0, 0, row + 1, 1)
|
||||
layout.setRowStretch(row, 1)
|
||||
row += 1
|
||||
|
||||
layout.addWidget(alpha_slider_proxy, row, 0)
|
||||
|
||||
layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col)
|
||||
layout.addWidget(alpha_inputs, row, input_col)
|
||||
row += 1
|
||||
layout.setRowStretch(row, 1)
|
||||
|
||||
color_view.set_color(color_triangle.cur_color)
|
||||
|
||||
color_triangle.color_changed.connect(self.triangle_color_changed)
|
||||
alpha_slider.valueChanged.connect(self._on_alpha_slider_change)
|
||||
pick_widget.color_selected.connect(self.on_color_change)
|
||||
alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed)
|
||||
btn_pick_color.released.connect(self.pick_color)
|
||||
|
||||
self.color_input_fields = color_input_fields
|
||||
self.color_inputs_color = color_inputs_color
|
||||
|
||||
self.pick_widget = pick_widget
|
||||
|
||||
self.color_triangle = color_triangle
|
||||
self.alpha_slider = alpha_slider
|
||||
|
||||
self.color_view = color_view
|
||||
self.alpha_inputs = alpha_inputs
|
||||
self.btn_pick_color = btn_pick_color
|
||||
|
||||
self._minimum_size_set = False
|
||||
|
||||
if color:
|
||||
self.set_color(color)
|
||||
self.alpha_changed(color.alpha())
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ColorPickerWidget, self).showEvent(event)
|
||||
if self._minimum_size_set:
|
||||
return
|
||||
|
||||
triangle_size = max(int(self.width() / 5 * 3), 180)
|
||||
self.color_triangle.setMinimumWidth(triangle_size)
|
||||
self.color_triangle.setMinimumHeight(triangle_size)
|
||||
self._minimum_size_set = True
|
||||
|
||||
def color(self):
|
||||
return self.color_view.color()
|
||||
|
||||
def set_color(self, color):
|
||||
self.alpha_inputs.set_alpha(color.alpha())
|
||||
self.on_color_change(color)
|
||||
|
||||
def pick_color(self):
|
||||
self.pick_widget.pick_color()
|
||||
|
||||
def triangle_color_changed(self, color):
|
||||
self.color_view.set_color(color)
|
||||
if self.color_inputs_color != color:
|
||||
self.color_inputs_color.setRgb(
|
||||
color.red(), color.green(), color.blue()
|
||||
)
|
||||
for color_input in self.color_input_fields:
|
||||
color_input.color_changed()
|
||||
|
||||
def on_color_change(self, color):
|
||||
self.color_view.set_color(color)
|
||||
self.color_triangle.set_color(color)
|
||||
if self.color_inputs_color != color:
|
||||
self.color_inputs_color.setRgb(
|
||||
color.red(), color.green(), color.blue()
|
||||
)
|
||||
for color_input in self.color_input_fields:
|
||||
color_input.color_changed()
|
||||
|
||||
def _on_color_input_value_change(self):
|
||||
for input_field in self.color_input_fields:
|
||||
input_field.color_changed()
|
||||
self.on_color_change(QtGui.QColor(self.color_inputs_color))
|
||||
|
||||
def alpha_changed(self, value):
|
||||
self.color_view.set_alpha(value)
|
||||
if self.alpha_slider.value() != value:
|
||||
self.alpha_slider.setValue(value)
|
||||
|
||||
if self.alpha_inputs.alpha_value != value:
|
||||
self.alpha_inputs.set_alpha(value)
|
||||
|
||||
def _on_alpha_inputs_changed(self, value):
|
||||
self.alpha_changed(value)
|
||||
|
||||
def _on_alpha_slider_change(self, value):
|
||||
self.alpha_changed(value)
|
||||
248
openpype/widgets/color_widgets/color_screen_pick.py
Normal file
248
openpype/widgets/color_widgets/color_screen_pick.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import Qt
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class PickScreenColorWidget(QtWidgets.QWidget):
|
||||
color_selected = QtCore.Signal(QtGui.QColor)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PickScreenColorWidget, self).__init__(parent)
|
||||
self.labels = []
|
||||
self.magnification = 2
|
||||
|
||||
self._min_magnification = 1
|
||||
self._max_magnification = 10
|
||||
|
||||
def add_magnification_delta(self, delta):
|
||||
_delta = abs(delta / 1000)
|
||||
if delta > 0:
|
||||
self.magnification += _delta
|
||||
else:
|
||||
self.magnification -= _delta
|
||||
|
||||
if self.magnification > self._max_magnification:
|
||||
self.magnification = self._max_magnification
|
||||
elif self.magnification < self._min_magnification:
|
||||
self.magnification = self._min_magnification
|
||||
|
||||
def pick_color(self):
|
||||
if self.labels:
|
||||
if self.labels[0].isVisible():
|
||||
return
|
||||
self.labels = []
|
||||
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
label = PickLabel(self)
|
||||
label.pick_color(screen)
|
||||
label.color_selected.connect(self.on_color_select)
|
||||
label.close_session.connect(self.end_pick_session)
|
||||
self.labels.append(label)
|
||||
|
||||
def end_pick_session(self):
|
||||
for label in self.labels:
|
||||
label.close()
|
||||
self.labels = []
|
||||
|
||||
def on_color_select(self, color):
|
||||
self.color_selected.emit(color)
|
||||
self.end_pick_session()
|
||||
|
||||
|
||||
class PickLabel(QtWidgets.QLabel):
|
||||
color_selected = QtCore.Signal(QtGui.QColor)
|
||||
close_session = QtCore.Signal()
|
||||
|
||||
def __init__(self, pick_widget):
|
||||
super(PickLabel, self).__init__()
|
||||
self.setMouseTracking(True)
|
||||
|
||||
self.pick_widget = pick_widget
|
||||
|
||||
self.radius_pen = QtGui.QPen(QtGui.QColor(27, 27, 27), 2)
|
||||
self.text_pen = QtGui.QPen(QtGui.QColor(127, 127, 127), 4)
|
||||
self.text_bg = QtGui.QBrush(QtGui.QColor(27, 27, 27))
|
||||
self._mouse_over = False
|
||||
|
||||
self.radius = 100
|
||||
self.radius_ratio = 11
|
||||
|
||||
@property
|
||||
def magnification(self):
|
||||
return self.pick_widget.magnification
|
||||
|
||||
def pick_color(self, screen_obj):
|
||||
self.show()
|
||||
self.windowHandle().setScreen(screen_obj)
|
||||
geo = screen_obj.geometry()
|
||||
args = (
|
||||
QtWidgets.QApplication.desktop().winId(),
|
||||
geo.x(), geo.y(), geo.width(), geo.height()
|
||||
)
|
||||
if Qt.__binding__ in ("PyQt4", "PySide"):
|
||||
pix = QtGui.QPixmap.grabWindow(*args)
|
||||
else:
|
||||
pix = screen_obj.grabWindow(*args)
|
||||
|
||||
if pix.width() > pix.height():
|
||||
size = pix.height()
|
||||
else:
|
||||
size = pix.width()
|
||||
|
||||
self.radius = int(size / self.radius_ratio)
|
||||
|
||||
self.setPixmap(pix)
|
||||
self.showFullScreen()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
y_delta = event.angleDelta().y()
|
||||
self.pick_widget.add_magnification_delta(y_delta)
|
||||
self.update()
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._mouse_over = True
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._mouse_over = False
|
||||
super().leaveEvent(event)
|
||||
self.update()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
if not self._mouse_over:
|
||||
return
|
||||
|
||||
mouse_pos_to_widet = self.mapFromGlobal(QtGui.QCursor.pos())
|
||||
|
||||
magnified_half_size = self.radius / self.magnification
|
||||
magnified_size = magnified_half_size * 2
|
||||
|
||||
zoom_x_1 = mouse_pos_to_widet.x() - magnified_half_size
|
||||
zoom_x_2 = mouse_pos_to_widet.x() + magnified_half_size
|
||||
zoom_y_1 = mouse_pos_to_widet.y() - magnified_half_size
|
||||
zoom_y_2 = mouse_pos_to_widet.y() + magnified_half_size
|
||||
pix_width = magnified_size
|
||||
pix_height = magnified_size
|
||||
draw_pos_x = 0
|
||||
draw_pos_y = 0
|
||||
if zoom_x_1 < 0:
|
||||
draw_pos_x = abs(zoom_x_1)
|
||||
pix_width -= draw_pos_x
|
||||
zoom_x_1 = 1
|
||||
elif zoom_x_2 > self.pixmap().width():
|
||||
pix_width -= zoom_x_2 - self.pixmap().width()
|
||||
|
||||
if zoom_y_1 < 0:
|
||||
draw_pos_y = abs(zoom_y_1)
|
||||
pix_height -= draw_pos_y
|
||||
zoom_y_1 = 1
|
||||
elif zoom_y_2 > self.pixmap().height():
|
||||
pix_height -= zoom_y_2 - self.pixmap().height()
|
||||
|
||||
new_pix = QtGui.QPixmap(magnified_size, magnified_size)
|
||||
new_pix.fill(QtCore.Qt.transparent)
|
||||
new_pix_painter = QtGui.QPainter(new_pix)
|
||||
new_pix_painter.drawPixmap(
|
||||
QtCore.QRect(draw_pos_x, draw_pos_y, pix_width, pix_height),
|
||||
self.pixmap().copy(zoom_x_1, zoom_y_1, pix_width, pix_height)
|
||||
)
|
||||
new_pix_painter.end()
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
ellipse_rect = QtCore.QRect(
|
||||
mouse_pos_to_widet.x() - self.radius,
|
||||
mouse_pos_to_widet.y() - self.radius,
|
||||
self.radius * 2,
|
||||
self.radius * 2
|
||||
)
|
||||
ellipse_rect_f = QtCore.QRectF(ellipse_rect)
|
||||
path = QtGui.QPainterPath()
|
||||
path.addEllipse(ellipse_rect_f)
|
||||
painter.setClipPath(path)
|
||||
|
||||
new_pix_rect = QtCore.QRect(
|
||||
mouse_pos_to_widet.x() - self.radius + 1,
|
||||
mouse_pos_to_widet.y() - self.radius + 1,
|
||||
new_pix.width() * self.magnification,
|
||||
new_pix.height() * self.magnification
|
||||
)
|
||||
|
||||
painter.drawPixmap(new_pix_rect, new_pix)
|
||||
|
||||
painter.setClipping(False)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
painter.setPen(self.radius_pen)
|
||||
painter.drawEllipse(ellipse_rect_f)
|
||||
|
||||
image = self.pixmap().toImage()
|
||||
if image.valid(mouse_pos_to_widet):
|
||||
color = QtGui.QColor(image.pixel(mouse_pos_to_widet))
|
||||
else:
|
||||
color = QtGui.QColor()
|
||||
|
||||
color_text = "Red: {} - Green: {} - Blue: {}".format(
|
||||
color.red(), color.green(), color.blue()
|
||||
)
|
||||
font = painter.font()
|
||||
font.setPointSize(self.radius / 10)
|
||||
painter.setFont(font)
|
||||
|
||||
text_rect_height = int(painter.fontMetrics().height() + 10)
|
||||
text_rect = QtCore.QRect(
|
||||
ellipse_rect.x(),
|
||||
ellipse_rect.bottom(),
|
||||
ellipse_rect.width(),
|
||||
text_rect_height
|
||||
)
|
||||
if text_rect.bottom() > self.pixmap().height():
|
||||
text_rect.moveBottomLeft(ellipse_rect.topLeft())
|
||||
|
||||
rect_radius = text_rect_height / 2
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(
|
||||
QtCore.QRectF(text_rect),
|
||||
rect_radius,
|
||||
rect_radius
|
||||
)
|
||||
painter.fillPath(path, self.text_bg)
|
||||
|
||||
painter.setPen(self.text_pen)
|
||||
painter.drawText(
|
||||
text_rect,
|
||||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignCenter,
|
||||
color_text
|
||||
)
|
||||
|
||||
color_rect_x = ellipse_rect.x() - text_rect_height
|
||||
if color_rect_x < 0:
|
||||
color_rect_x += (text_rect_height + ellipse_rect.width())
|
||||
|
||||
color_rect = QtCore.QRect(
|
||||
color_rect_x,
|
||||
ellipse_rect.y(),
|
||||
text_rect_height,
|
||||
ellipse_rect.height()
|
||||
)
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(
|
||||
QtCore.QRectF(color_rect),
|
||||
rect_radius,
|
||||
rect_radius
|
||||
)
|
||||
painter.fillPath(path, color)
|
||||
painter.drawRoundedRect(color_rect, rect_radius, rect_radius)
|
||||
painter.end()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
color = QtGui.QColor(self.pixmap().toImage().pixel(event.pos()))
|
||||
self.color_selected.emit(color)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self.close_session.emit()
|
||||
1431
openpype/widgets/color_widgets/color_triangle.py
Normal file
1431
openpype/widgets/color_widgets/color_triangle.py
Normal file
File diff suppressed because it is too large
Load diff
83
openpype/widgets/color_widgets/color_view.py
Normal file
83
openpype/widgets/color_widgets/color_view.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None):
|
||||
if piece_size is None:
|
||||
piece_size = 7
|
||||
|
||||
if color_1 is None:
|
||||
color_1 = QtGui.QColor(188, 188, 188)
|
||||
|
||||
if color_2 is None:
|
||||
color_2 = QtGui.QColor(90, 90, 90)
|
||||
|
||||
pix = QtGui.QPixmap(piece_size * 2, piece_size * 2)
|
||||
pix_painter = QtGui.QPainter(pix)
|
||||
|
||||
rect = QtCore.QRect(
|
||||
0, 0, piece_size, piece_size
|
||||
)
|
||||
pix_painter.fillRect(rect, color_1)
|
||||
rect.moveTo(piece_size, piece_size)
|
||||
pix_painter.fillRect(rect, color_1)
|
||||
rect.moveTo(piece_size, 0)
|
||||
pix_painter.fillRect(rect, color_2)
|
||||
rect.moveTo(0, piece_size)
|
||||
pix_painter.fillRect(rect, color_2)
|
||||
pix_painter.end()
|
||||
|
||||
return pix
|
||||
|
||||
|
||||
class ColorViewer(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(ColorViewer, self).__init__(parent)
|
||||
|
||||
self.setMinimumSize(10, 10)
|
||||
|
||||
self.alpha = 255
|
||||
self.actual_pen = QtGui.QPen()
|
||||
self.actual_color = QtGui.QColor()
|
||||
self._checkerboard = None
|
||||
|
||||
def checkerboard(self):
|
||||
if not self._checkerboard:
|
||||
self._checkerboard = draw_checkerboard_tile(4)
|
||||
return self._checkerboard
|
||||
|
||||
def color(self):
|
||||
return self.actual_color
|
||||
|
||||
def set_color(self, color):
|
||||
if color == self.actual_color:
|
||||
return
|
||||
|
||||
# Create copy of entered color
|
||||
self.actual_color = QtGui.QColor(color)
|
||||
# Set alpha by current alpha value
|
||||
self.actual_color.setAlpha(self.alpha)
|
||||
# Repaint
|
||||
self.update()
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
if alpha == self.alpha:
|
||||
return
|
||||
# Change alpha of current color
|
||||
self.actual_color.setAlpha(alpha)
|
||||
# Store the value
|
||||
self.alpha = alpha
|
||||
# Repaint
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
clip_rect = event.rect()
|
||||
rect = clip_rect.adjusted(0, 0, -1, -1)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setClipRect(clip_rect)
|
||||
painter.drawTiledPixmap(rect, self.checkerboard())
|
||||
painter.setBrush(self.actual_color)
|
||||
pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(rect)
|
||||
painter.end()
|
||||
BIN
openpype/widgets/color_widgets/eyedropper.png
Normal file
BIN
openpype/widgets/color_widgets/eyedropper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.0.0-rc.5"
|
||||
version = "3.0.0-rc.6"
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a
|
||||
Subproject commit 0d9a228fdb2eb08fe6caa30f25fe2a34fead1a03
|
||||
123
start.py
123
start.py
|
|
@ -6,10 +6,11 @@ Bootstrapping process of OpenPype is as follows:
|
|||
`OPENPYPE_PATH` is checked for existence - either one from environment or
|
||||
from user settings. Precedence takes the one set by environment.
|
||||
|
||||
On this path we try to find OpenPype in directories version string in their names.
|
||||
For example: `openpype-v3.0.1-foo` is valid name, or even `foo_3.0.2` - as long
|
||||
as version can be determined from its name _AND_ file `openpype/openpype/version.py`
|
||||
can be found inside, it is considered OpenPype installation.
|
||||
On this path we try to find OpenPype in directories version string in their
|
||||
names. For example: `openpype-v3.0.1-foo` is valid name, or
|
||||
even `foo_3.0.2` - as long as version can be determined from its name
|
||||
_AND_ file `openpype/openpype/version.py` can be found inside, it is
|
||||
considered OpenPype installation.
|
||||
|
||||
If no OpenPype repositories are found in `OPENPYPE_PATH` (user data dir)
|
||||
then **Igniter** (OpenPype setup tool) will launch its GUI.
|
||||
|
|
@ -20,19 +21,19 @@ appdata dir in user home and extract it there. Version will be determined by
|
|||
version specified in OpenPype module.
|
||||
|
||||
If OpenPype repository directories are found in default install location
|
||||
(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs there and
|
||||
use latest one or the one specified with optional `--use-version` command
|
||||
line argument. If the one specified doesn't exist then latest available
|
||||
version will be used. All repositories in that dir will be added
|
||||
(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs
|
||||
there and use latest one or the one specified with optional `--use-version`
|
||||
command line argument. If the one specified doesn't exist then latest
|
||||
available version will be used. All repositories in that dir will be added
|
||||
to `sys.path` and `PYTHONPATH`.
|
||||
|
||||
If OpenPype is live (not frozen) then current version of OpenPype module will be
|
||||
used. All directories under `repos` will be added to `sys.path` and
|
||||
If OpenPype is live (not frozen) then current version of OpenPype module
|
||||
will be used. All directories under `repos` will be added to `sys.path` and
|
||||
`PYTHONPATH`.
|
||||
|
||||
OpenPype depends on connection to `MongoDB`_. You can specify MongoDB connection
|
||||
string via `OPENPYPE_MONGO` set in environment or it can be set in user
|
||||
settings or via **Igniter** GUI.
|
||||
OpenPype depends on connection to `MongoDB`_. You can specify MongoDB
|
||||
connection string via `OPENPYPE_MONGO` set in environment or it can be set
|
||||
in user settings or via **Igniter** GUI.
|
||||
|
||||
So, bootstrapping OpenPype looks like this::
|
||||
|
||||
|
|
@ -282,7 +283,8 @@ def _process_arguments() -> tuple:
|
|||
print(" --use-version=3.0.0")
|
||||
sys.exit(1)
|
||||
|
||||
m = re.search(r"--use-version=(?P<version>\d+\.\d+\.\d*.+?)", arg)
|
||||
m = re.search(
|
||||
r"--use-version=(?P<version>\d+\.\d+\.\d+(?:\S*)?)", arg)
|
||||
if m and m.group('version'):
|
||||
use_version = m.group('version')
|
||||
sys.argv.remove(arg)
|
||||
|
|
@ -414,6 +416,7 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
(if requested).
|
||||
|
||||
"""
|
||||
version_path = None
|
||||
openpype_version = None
|
||||
openpype_versions = bootstrap.find_openpype(include_zips=True,
|
||||
staging=use_staging)
|
||||
|
|
@ -433,7 +436,6 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
if local_version == openpype_versions[-1]:
|
||||
os.environ["OPENPYPE_TRYOUT"] = "1"
|
||||
openpype_versions = []
|
||||
|
||||
else:
|
||||
print("!!! Warning: cannot determine current running version.")
|
||||
|
||||
|
|
@ -480,17 +482,25 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
return version_path
|
||||
|
||||
# get path of version specified in `--use-version`
|
||||
version_path = BootstrapRepos.get_version_path_from_list(
|
||||
use_version, openpype_versions)
|
||||
local_version = bootstrap.get_version(OPENPYPE_ROOT)
|
||||
if use_version and use_version != local_version:
|
||||
# force the one user has selected
|
||||
openpype_version = None
|
||||
openpype_versions = bootstrap.find_openpype(include_zips=True,
|
||||
staging=use_staging)
|
||||
v: OpenPypeVersion
|
||||
found = [v for v in openpype_versions if str(v) == use_version]
|
||||
if found:
|
||||
openpype_version = sorted(found)[-1]
|
||||
if not openpype_version:
|
||||
print(f"!!! requested version {use_version} was not found.")
|
||||
if openpype_versions:
|
||||
print(" - found: ")
|
||||
for v in sorted(openpype_versions):
|
||||
print(f" - {v}: {v.path}")
|
||||
|
||||
if not version_path:
|
||||
if use_version is not None and openpype_version:
|
||||
print(("!!! Specified version was not found, using "
|
||||
"latest available"))
|
||||
# specified version was not found so use latest detected.
|
||||
version_path = openpype_version.path
|
||||
print(f">>> Using version [ {openpype_version} ]")
|
||||
print(f" From {version_path}")
|
||||
print(f" - local version {local_version}")
|
||||
sys.exit(1)
|
||||
|
||||
# test if latest detected is installed (in user data dir)
|
||||
is_inside = False
|
||||
|
|
@ -521,7 +531,7 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
openpype_version.path = version_path
|
||||
|
||||
_initialize_environment(openpype_version)
|
||||
return version_path
|
||||
return openpype_version.path
|
||||
|
||||
|
||||
def _bootstrap_from_code(use_version):
|
||||
|
|
@ -536,36 +546,53 @@ def _bootstrap_from_code(use_version):
|
|||
"""
|
||||
# run through repos and add them to `sys.path` and `PYTHONPATH`
|
||||
# set root
|
||||
_openpype_root = OPENPYPE_ROOT
|
||||
if getattr(sys, 'frozen', False):
|
||||
local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))
|
||||
local_version = bootstrap.get_version(Path(_openpype_root))
|
||||
print(f" - running version: {local_version}")
|
||||
assert local_version
|
||||
else:
|
||||
# get current version of OpenPype
|
||||
local_version = bootstrap.get_local_live_version()
|
||||
|
||||
os.environ["OPENPYPE_VERSION"] = local_version
|
||||
if use_version and use_version != local_version:
|
||||
version_to_use = None
|
||||
openpype_versions = bootstrap.find_openpype(include_zips=True)
|
||||
version_path = BootstrapRepos.get_version_path_from_list(
|
||||
use_version, openpype_versions)
|
||||
if version_path:
|
||||
# use specified
|
||||
bootstrap.add_paths_from_directory(version_path)
|
||||
os.environ["OPENPYPE_VERSION"] = use_version
|
||||
else:
|
||||
version_path = OPENPYPE_ROOT
|
||||
v: OpenPypeVersion
|
||||
found = [v for v in openpype_versions if str(v) == use_version]
|
||||
if found:
|
||||
version_to_use = sorted(found)[-1]
|
||||
|
||||
repos = os.listdir(os.path.join(OPENPYPE_ROOT, "repos"))
|
||||
repos = [os.path.join(OPENPYPE_ROOT, "repos", repo) for repo in repos]
|
||||
if version_to_use:
|
||||
# use specified
|
||||
if version_to_use.path.is_file():
|
||||
version_to_use.path = bootstrap.extract_openpype(
|
||||
version_to_use)
|
||||
bootstrap.add_paths_from_directory(version_to_use.path)
|
||||
os.environ["OPENPYPE_VERSION"] = use_version
|
||||
version_path = version_to_use.path
|
||||
os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501
|
||||
_openpype_root = version_to_use.path.as_posix()
|
||||
else:
|
||||
print(f"!!! requested version {use_version} was not found.")
|
||||
if openpype_versions:
|
||||
print(" - found: ")
|
||||
for v in sorted(openpype_versions):
|
||||
print(f" - {v}: {v.path}")
|
||||
|
||||
print(f" - local version {local_version}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
os.environ["OPENPYPE_VERSION"] = local_version
|
||||
version_path = Path(_openpype_root)
|
||||
os.environ["OPENPYPE_REPOS_ROOT"] = _openpype_root
|
||||
|
||||
repos = os.listdir(os.path.join(_openpype_root, "repos"))
|
||||
repos = [os.path.join(_openpype_root, "repos", repo) for repo in repos]
|
||||
# add self to python paths
|
||||
repos.insert(0, OPENPYPE_ROOT)
|
||||
repos.insert(0, _openpype_root)
|
||||
for repo in repos:
|
||||
sys.path.insert(0, repo)
|
||||
|
||||
# Set OPENPYPE_REPOS_ROOT to code root
|
||||
os.environ["OPENPYPE_REPOS_ROOT"] = OPENPYPE_ROOT
|
||||
|
||||
# add venv 'site-packages' to PYTHONPATH
|
||||
python_path = os.getenv("PYTHONPATH", "")
|
||||
split_paths = python_path.split(os.pathsep)
|
||||
|
|
@ -580,11 +607,11 @@ def _bootstrap_from_code(use_version):
|
|||
# point to same hierarchy from code and from frozen OpenPype
|
||||
additional_paths = [
|
||||
# add OpenPype tools
|
||||
os.path.join(OPENPYPE_ROOT, "openpype", "tools"),
|
||||
os.path.join(_openpype_root, "openpype", "tools"),
|
||||
# add common OpenPype vendor
|
||||
# (common for multiple Python interpreter versions)
|
||||
os.path.join(
|
||||
OPENPYPE_ROOT,
|
||||
_openpype_root,
|
||||
"openpype",
|
||||
"vendor",
|
||||
"python",
|
||||
|
|
@ -597,7 +624,7 @@ def _bootstrap_from_code(use_version):
|
|||
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join(split_paths)
|
||||
|
||||
return Path(version_path)
|
||||
return version_path
|
||||
|
||||
|
||||
def boot():
|
||||
|
|
@ -624,6 +651,10 @@ def boot():
|
|||
|
||||
use_version, use_staging = _process_arguments()
|
||||
|
||||
if os.getenv("OPENPYPE_VERSION"):
|
||||
use_staging = "staging" in os.getenv("OPENPYPE_VERSION")
|
||||
use_version = os.getenv("OPENPYPE_VERSION")
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Determine mongodb connection
|
||||
# ------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -175,9 +175,9 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
|
|||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
Write-Host "Cleaning cache files ... " -NoNewline
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
|
||||
Write-Host "OK" -ForegroundColor green
|
||||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ clean_pyc () {
|
|||
local path
|
||||
path=$openpype_root
|
||||
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
|
||||
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
|
||||
echo -e "${BIGreen}DONE${RST}"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,16 +76,20 @@ print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))
|
|||
Set-Location -Path $current_dir
|
||||
Exit-WithCode 1
|
||||
}
|
||||
# We are supporting python 3.6 and up
|
||||
if(($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
|
||||
# We are supporting python 3.7 only
|
||||
if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
|
||||
Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red
|
||||
Set-Location -Path $current_dir
|
||||
Exit-WithCode 1
|
||||
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) {
|
||||
Write-Host "WARNING Version [ $p ] is unsupported, use at your own risk." -ForegroundColor yellow
|
||||
Write-Host "*** " -NoNewline -ForegroundColor yellow
|
||||
Write-Host "OpenPype supports only Python 3.7" -ForegroundColor white
|
||||
} else {
|
||||
Write-Host "OK [ $p ]" -ForegroundColor green
|
||||
}
|
||||
Write-Host "OK [ $p ]" -ForegroundColor green
|
||||
}
|
||||
|
||||
|
||||
$current_dir = Get-Location
|
||||
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
|
||||
$openpype_root = (Get-Item $script_dir).parent.FullName
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ clean_pyc () {
|
|||
local path
|
||||
path=$openpype_root
|
||||
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
|
||||
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
echo -e "${BIGreen}DONE${RST}"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,9 +98,9 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
|
|||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
Write-Host "Cleaning cache files ... " -NoNewline
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse| Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
|
||||
Write-Host "OK" -ForegroundColor green
|
||||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
|
|
|
|||
|
|
@ -89,23 +89,6 @@ detect_python () {
|
|||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Clean pyc files in specified directory
|
||||
# Globals:
|
||||
# None
|
||||
# Arguments:
|
||||
# Optional path to clean
|
||||
# Returns:
|
||||
# None
|
||||
###############################################################################
|
||||
clean_pyc () {
|
||||
local path
|
||||
path=$openpype_root
|
||||
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
|
||||
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
echo -e "${BIGreen}DONE${RST}"
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Return absolute path
|
||||
# Globals:
|
||||
|
|
|
|||
|
|
@ -10,9 +10,51 @@
|
|||
PS> .\run_project_manager.ps1
|
||||
|
||||
#>
|
||||
|
||||
$art = @"
|
||||
|
||||
. . .. . ..
|
||||
_oOOP3OPP3Op_. .
|
||||
.PPpo~. .. ~2p. .. .... . .
|
||||
.Ppo . .pPO3Op.. . O:. . . .
|
||||
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
|
||||
.~OP 3PO. .Op3 : . .. _____ _____ _____
|
||||
.P3O . oP3oP3O3P' . . . . / /./ /./ /
|
||||
O3:. O3p~ . .:. . ./____/./____/ /____/
|
||||
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
|
||||
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
|
||||
. '_ .. . . _OP3.. . .https://openpype.io.. .
|
||||
~P3.OPPPO3OP~ . .. .
|
||||
. ' '. . .. . . . .. .
|
||||
|
||||
"@
|
||||
|
||||
Write-Host $art -ForegroundColor DarkGreen
|
||||
|
||||
$current_dir = Get-Location
|
||||
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
|
||||
$openpype_root = (Get-Item $script_dir).parent.FullName
|
||||
|
||||
$env:_INSIDE_OPENPYPE_TOOL = "1"
|
||||
|
||||
# make sure Poetry is in PATH
|
||||
if (-not (Test-Path 'env:POETRY_HOME')) {
|
||||
$env:POETRY_HOME = "$openpype_root\.poetry"
|
||||
}
|
||||
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
|
||||
|
||||
Set-Location -Path $openpype_root
|
||||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor Green
|
||||
Write-Host "Reading Poetry ... " -NoNewline
|
||||
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
|
||||
Write-Host "NOT FOUND" -ForegroundColor Yellow
|
||||
Write-Host "*** " -NoNewline -ForegroundColor Yellow
|
||||
Write-Host "We need to install Poetry create virtual env first ..."
|
||||
& "$openpype_root\tools\create_env.ps1"
|
||||
} else {
|
||||
Write-Host "OK" -ForegroundColor Green
|
||||
}
|
||||
|
||||
& poetry run python "$($openpype_root)\start.py" projectmanager
|
||||
Set-Location -Path $current_dir
|
||||
|
|
|
|||
103
tools/run_projectmanager.sh
Executable file
103
tools/run_projectmanager.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Run OpenPype Settings GUI
|
||||
|
||||
|
||||
art () {
|
||||
cat <<-EOF
|
||||
|
||||
. . .. . ..
|
||||
_oOOP3OPP3Op_. .
|
||||
.PPpo~· ·· ~2p. ·· ···· · ·
|
||||
·Ppo · .pPO3Op.· · O:· · · ·
|
||||
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
|
||||
·~OP 3PO· .Op3 : · ·· _____ _____ _____
|
||||
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
|
||||
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
|
||||
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
|
||||
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
|
||||
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
|
||||
~P3·OPPPO3OP~ · ·· ·
|
||||
· ' '· · ·· · · · ·· ·
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Colors for terminal
|
||||
|
||||
RST='\033[0m' # Text Reset
|
||||
|
||||
# Regular Colors
|
||||
Black='\033[0;30m' # Black
|
||||
Red='\033[0;31m' # Red
|
||||
Green='\033[0;32m' # Green
|
||||
Yellow='\033[0;33m' # Yellow
|
||||
Blue='\033[0;34m' # Blue
|
||||
Purple='\033[0;35m' # Purple
|
||||
Cyan='\033[0;36m' # Cyan
|
||||
White='\033[0;37m' # White
|
||||
|
||||
# Bold
|
||||
BBlack='\033[1;30m' # Black
|
||||
BRed='\033[1;31m' # Red
|
||||
BGreen='\033[1;32m' # Green
|
||||
BYellow='\033[1;33m' # Yellow
|
||||
BBlue='\033[1;34m' # Blue
|
||||
BPurple='\033[1;35m' # Purple
|
||||
BCyan='\033[1;36m' # Cyan
|
||||
BWhite='\033[1;37m' # White
|
||||
|
||||
# Bold High Intensity
|
||||
BIBlack='\033[1;90m' # Black
|
||||
BIRed='\033[1;91m' # Red
|
||||
BIGreen='\033[1;92m' # Green
|
||||
BIYellow='\033[1;93m' # Yellow
|
||||
BIBlue='\033[1;94m' # Blue
|
||||
BIPurple='\033[1;95m' # Purple
|
||||
BICyan='\033[1;96m' # Cyan
|
||||
BIWhite='\033[1;97m' # White
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Return absolute path
|
||||
# Globals:
|
||||
# None
|
||||
# Arguments:
|
||||
# Path to resolve
|
||||
# Returns:
|
||||
# None
|
||||
###############################################################################
|
||||
realpath () {
|
||||
echo $(cd $(dirname "$1"); pwd)/$(basename "$1")
|
||||
}
|
||||
|
||||
# Main
|
||||
main () {
|
||||
|
||||
# Directories
|
||||
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
|
||||
|
||||
_inside_openpype_tool="1"
|
||||
|
||||
# make sure Poetry is in PATH
|
||||
if [[ -z $POETRY_HOME ]]; then
|
||||
export POETRY_HOME="$openpype_root/.poetry"
|
||||
fi
|
||||
export PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
pushd "$openpype_root" > /dev/null || return > /dev/null
|
||||
|
||||
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
|
||||
if [ -f "$POETRY_HOME/bin/poetry" ]; then
|
||||
echo -e "${BIGreen}OK${RST}"
|
||||
else
|
||||
echo -e "${BIYellow}NOT FOUND${RST}"
|
||||
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
|
||||
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
|
||||
fi
|
||||
|
||||
echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..."
|
||||
poetry run python "$openpype_root/start.py" projectmanager
|
||||
}
|
||||
|
||||
main
|
||||
|
|
@ -94,8 +94,8 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
|
|||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
Write-Host "Cleaning cache files ... " -NoNewline
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
|
||||
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
|
||||
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
|
||||
Write-Host "OK" -ForegroundColor green
|
||||
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor green
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ clean_pyc () {
|
|||
local path
|
||||
path=$openpype_root
|
||||
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
|
||||
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
|
||||
echo -e "${BIGreen}DONE${RST}"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,26 @@ version to run for the artist, until a higher version is detected in the update
|
|||
#### Manual Updates
|
||||
|
||||
If for some reason you don't want to use the automatic updates, you can distribute your
|
||||
zips manually. Your artist will then have to unpack them to the correct place on their disk.
|
||||
zips manually. Your artist will then have to put them to the correct place on their disk.
|
||||
Zips will be automatically unzipped there.
|
||||
|
||||
The default locations are:
|
||||
|
||||
- Windows: `C:\Users\%USERNAME%\AppData\Local\pypeclub\openpype`
|
||||
- Linux: ` `
|
||||
- Mac: ` `
|
||||
- Windows: `%LOCALAPPDATA%\pypeclub\openpype`
|
||||
- Linux: `~/.local/share/pypeclub/openpype`
|
||||
- Mac: `~/Library/Application Support/pypeclub/openpype`
|
||||
|
||||
|
||||
### Staging vs. Production
|
||||
You can have version of OpenPype with experimental features you want to try somewhere but you
|
||||
don't want to disrupt your production. You can tag version as **staging** simply by appending `+staging`
|
||||
to its name.
|
||||
|
||||
So if you have OpenPype version like `OpenPype-v3.0.0.zip` just name it `OpenPype-v3.0.0+staging.zip`.
|
||||
When both these versions are present, production one will always take precedence over staging.
|
||||
|
||||
You can run OpenPype with `--use-staging` argument to add use staging versions.
|
||||
|
||||
:::note
|
||||
Running staging version is identified by orange **P** icon in system tray.
|
||||
:::
|
||||
|
|
@ -4,137 +4,154 @@ title: OpenPype Commands Reference
|
|||
sidebar_label: OpenPype Commands
|
||||
---
|
||||
|
||||
:::info
|
||||
You can substitute `openpype_console` with `poetry run python start.py` if you want to run it
|
||||
directly from sources.
|
||||
:::
|
||||
|
||||
## `tray`
|
||||
:::note
|
||||
Running OpenPype without any commands will default to `tray`.
|
||||
:::
|
||||
|
||||
To launch Tray:
|
||||
```sh
|
||||
pype tray
|
||||
## Common arguments
|
||||
`--use-version` to specify explicit version to use:
|
||||
```shell
|
||||
openpype_console --use-version=3.0.0-foo+bar
|
||||
```
|
||||
|
||||
### `--debug`
|
||||
`--use-staging` - to use staging versions of OpenPype.
|
||||
|
||||
For more information [see here](admin_use#run-openpype).
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | Arguments |
|
||||
| --- | --- |: --- :|
|
||||
| tray | Launch OpenPype Tray. | [📑](#tray-arguments)
|
||||
| eventserver | This should be ideally used by system service (such as systemd or upstart on linux and window service). | [📑](#eventserver-arguments) |
|
||||
| launch | Launch application in Pype environment. | [📑](#launch-arguments) |
|
||||
| publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) |
|
||||
| extractenvironments | Extract environment variables for entered context to a json file. | [📑](#extractenvironments-arguments) |
|
||||
| run | Execute given python script within OpenPype environment. | [📑](#run-arguments) |
|
||||
| projectmanager | Launch Project Manager UI | [📑](#projectmanager-arguments) |
|
||||
| settings | Open Settings UI | [📑](#settings-arguments) |
|
||||
| standalonepublisher | Open Standalone Publisher UI | [📑](#standalonepublisher-arguments) |
|
||||
|
||||
---
|
||||
### `tray` arguments {#tray-arguments}
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `--debug` | print verbose information useful for debugging (works with `openpype_console`) |
|
||||
|
||||
To launch Tray with debugging information:
|
||||
```sh
|
||||
pype tray --debug
|
||||
```shell
|
||||
openpype_console tray --debug
|
||||
```
|
||||
---
|
||||
### `launch` arguments {#eventserver-arguments}
|
||||
You have to set either proper environment variables to provide URL and credentials or use
|
||||
option to specify them. If you use `--store_credentials` provided credentials will be stored for later use.
|
||||
|
||||
--------------------
|
||||
|
||||
|
||||
## `eventserver`
|
||||
|
||||
This command launches ftrack event server.
|
||||
|
||||
This should be ideally used by system service (such us systemd or upstart
|
||||
on linux and window service).
|
||||
|
||||
You have to set either proper environment variables to provide URL and
|
||||
credentials or use option to specify them. If you use `--store_credentials`
|
||||
provided credentials will be stored for later use.
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `--debug` | print debug info |
|
||||
| `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) |
|
||||
| `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) |
|
||||
| `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) |
|
||||
| `--ftrack-events-path` | path to event server plugins (can be set with `FTRACK_EVENTS_PATH`) |
|
||||
| `--no-stored-credentials` | will use credential specified with options above |
|
||||
| `--store-credentials` | will store credentials to file for later use |
|
||||
| `--legacy` | run event server without mongo storing |
|
||||
| `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) |
|
||||
| `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) |
|
||||
|
||||
To run ftrack event server:
|
||||
```sh
|
||||
pype eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-api-key=<key> --ftrack-events-path=<path> --no-stored-credentials --store-credentials
|
||||
```shell
|
||||
openpype_console eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-api-key=<key> --ftrack-events-path=<path> --no-stored-credentials --store-credentials
|
||||
```
|
||||
|
||||
---
|
||||
### `launch` arguments {#launch-arguments}
|
||||
|
||||
### `--debug`
|
||||
- print debug info
|
||||
|
||||
### `--ftrack-url`
|
||||
- URL to ftrack server
|
||||
|
||||
### `--ftrack-user`
|
||||
- user name to log in to ftrack
|
||||
|
||||
### `--ftrack-api-key`
|
||||
- ftrack api key
|
||||
|
||||
### `--ftrack-events-path`
|
||||
- path to event server plugins
|
||||
|
||||
### `--no-stored-credentials`
|
||||
- will use credential specified with options above
|
||||
|
||||
### `--store-credentials`
|
||||
- will store credentials to file for later use
|
||||
|
||||
--------------------
|
||||
|
||||
## `launch`
|
||||
|
||||
Launch application in Pype environment.
|
||||
|
||||
### `--app`
|
||||
|
||||
Application name - this should be the same as it's [defining toml](admin_hosts#launchers) file (without .toml)
|
||||
|
||||
### `--project`
|
||||
Project name
|
||||
|
||||
### `--asset`
|
||||
Asset name
|
||||
|
||||
### `--task`
|
||||
Task name
|
||||
|
||||
### `--tools`
|
||||
*Optional: Additional tools environment files to add*
|
||||
|
||||
### `--user`
|
||||
*Optional: User on behalf to run*
|
||||
|
||||
### `--ftrack-server` / `-fs`
|
||||
*Optional: Ftrack server URL*
|
||||
|
||||
### `--ftrack-user` / `-fu`
|
||||
*Optional: Ftrack user*
|
||||
|
||||
### `--ftrack-key` / `-fk`
|
||||
*Optional: Ftrack API key*
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `--app` | Application name - this should be the key for application from Settings. |
|
||||
| `--project` | Project name (default taken from `AVALON_PROJECT` if set) |
|
||||
| `--asset` | Asset name (default taken from `AVALON_ASSET` if set) |
|
||||
| `--task` | Task name (default taken from `AVALON_TASK` is set) |
|
||||
| `--tools` | *Optional: Additional tools to add* |
|
||||
| `--user` | *Optional: User on behalf to run* |
|
||||
| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* |
|
||||
| `--ftrack-user` / `-fu` | *Optional: Ftrack user* |
|
||||
| `--ftrack-key` / `-fk` | *Optional: Ftrack API key* |
|
||||
|
||||
For example to run Python interactive console in Pype context:
|
||||
```sh
|
||||
```shell
|
||||
pype launch --app python --project my_project --asset my_asset --task my_task
|
||||
```
|
||||
|
||||
--------------------
|
||||
---
|
||||
### `publish` arguments {#publish-arguments}
|
||||
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `--debug` | print more verbose infomation |
|
||||
|
||||
## `publish`
|
||||
|
||||
Pype takes JSON from provided path and use it to publish data in it.
|
||||
```sh
|
||||
```shell
|
||||
pype publish <PATH_TO_JSON>
|
||||
```
|
||||
|
||||
### `--debug`
|
||||
- print more verbose infomation
|
||||
|
||||
--------------------
|
||||
|
||||
## `extractenvironments`
|
||||
|
||||
Extract environment variables for entered context to a json file.
|
||||
---
|
||||
### `extractenvironments` arguments {#extractenvironments-arguments}
|
||||
|
||||
Entered output filepath will be created if does not exists.
|
||||
|
||||
All context options must be passed otherwise only openpype's global environments will be extracted.
|
||||
Context options are `project`, `asset`, `task`, `app`
|
||||
|
||||
Context options are "project", "asset", "task", "app"
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `output_json_path` | Absolute path to the exported json file |
|
||||
| `--project` | Project name |
|
||||
| `--asset` | Asset name |
|
||||
| `--task` | Task name |
|
||||
| `--app` | Application name |
|
||||
|
||||
### `output_json_path`
|
||||
- Absolute path to the exported json file
|
||||
```shell
|
||||
openpype_console /home/openpype/env.json --project Foo --asset Bar --task modeling --app maya-2019
|
||||
```
|
||||
|
||||
### `--project`
|
||||
- Project name
|
||||
---
|
||||
### `run` arguments {#run-arguments}
|
||||
|
||||
### `--asset`
|
||||
- Asset name
|
||||
| Argument | Description |
|
||||
| `--script` | run specified python script |
|
||||
|
||||
### `--task`
|
||||
- Task name
|
||||
Note that additional arguments are passed to the script.
|
||||
|
||||
### `--app`
|
||||
- Application name
|
||||
```shell
|
||||
openpype_console run --script /foo/bar/baz.py arg1 arg2
|
||||
```
|
||||
|
||||
---
|
||||
### `projectmanager` arguments {#projectmanager-arguments}
|
||||
`projectmanager` has no command-line arguments.
|
||||
```shell
|
||||
openpype_console projectmanager
|
||||
```
|
||||
|
||||
---
|
||||
### `settings` arguments {#settings-arguments}
|
||||
|
||||
| Argument | Description |
|
||||
| `-d` / `--dev` | Run settings in developer mode. |
|
||||
|
||||
```shell
|
||||
openpypeconsole settings
|
||||
```
|
||||
|
||||
---
|
||||
### `standalonepublisher` arguments {#standalonepublisher-arguments}
|
||||
`standalonepublisher` has no command-line arguments.
|
||||
```shell
|
||||
openpype_console standalonepublisher
|
||||
```
|
||||
|
|
@ -32,6 +32,60 @@ Once artist enters the Mongo URL address, OpenPype will remember the connection
|
|||
next launch, so it is a one time process.From that moment OpenPype will do it's best to
|
||||
always keep up to date with the latest studio updates.
|
||||
|
||||
If the launch was successfull, the artist should see a green OpenPype logo in their
|
||||
If the launch was successful, the artist should see a green OpenPype logo in their
|
||||
tray menu. Keep in mind that on Windows this icon might be hidden by default, in which case,
|
||||
the artist can simply drag the icon down to the tray.
|
||||
the artist can simply drag the icon down to the tray.
|
||||
|
||||
You can use following command line arguments:
|
||||
|
||||
`--use-version` - to specify version you want to run explicitly, like:
|
||||
```shell
|
||||
openpype_console --use-version=3.0.1
|
||||
```
|
||||
|
||||
`--use-staging` - to specify you prefer staging version. In that case it will be used
|
||||
(if found) instead of production one.
|
||||
|
||||
### Details
|
||||
When you run OpenPype from executable, few check are made:
|
||||
|
||||
#### Check for mongoDB database connection
|
||||
MongoDB connection string is in format:
|
||||
```shell
|
||||
mongodb[+srv]://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]
|
||||
```
|
||||
More on that in [MongoDB documentation](https://docs.mongodb.com/manual/reference/connection-string/).
|
||||
|
||||
Example connection strings are `mongodb://local-unprotected-server:2707` or
|
||||
`mongodb+srv://user:superpassword:some.mongodb-hosted-on.net:27072`.
|
||||
|
||||
When you start OpenPype first time, Igniter UI will show up and ask you for this string. It will then
|
||||
save it in secure way to your systems keyring - on Windows it is **Credential Manager**, on MacOS it will use its
|
||||
**Keychain**, on Linux it can be **GNOME Keyring** or other software, depending on your distribution.
|
||||
|
||||
This can be also set beforehand with environment variable `OPENPYPE_MONGO`. If set it takes precedence
|
||||
over the one set in keyring.
|
||||
|
||||
#### Check for OpenPype version path
|
||||
When connection to MongoDB is made, OpenPype will get various settings from there - one among them is
|
||||
directory location where OpenPype versions are stored. If this directory exists OpenPype tries to
|
||||
find the latest version there and if succeed it will copy it to user system and use it.
|
||||
|
||||
This path can be set is OpenPype settings, but also with environment variable `OPENPYPE_PATH` or with
|
||||
`openPypePath` in json file located application directory depending on your system.
|
||||
|
||||
- Windows: `%LOCALAPPDATA%\pypeclub\openpype`
|
||||
- Linux: `~/.local/share/pypeclub/openpype`
|
||||
- Mac: `~/Library/Application Support/pypeclub/openpype`
|
||||
|
||||
### Runtime provided environment variables
|
||||
OpenPype is providing following environment variables for its subprocesses that can be used
|
||||
in various places, like scripting, etc.
|
||||
|
||||
- `OPENPYPE_ROOT` - this will point to currently running code.
|
||||
- `OPENPYPE_VERSION` - string of current version - like `3.0.0-foo+bar`
|
||||
- `OPENPYPE_REPOS_ROOT` - this is path where all components can be find (like Avalon Core and OpenPype)
|
||||
- `OPENPYPE_DATABASE_NAME` - database name in MongoDB used by OpenPype
|
||||
- `OPENPYPE_EXECUTABLE` - path to executable used to run OpenPype - when run from sources it will point
|
||||
to **python** stored in virtual environment. If run from frozen code, it will point to either `openpype_gui` or
|
||||
`openpype_console`.
|
||||
|
|
|
|||
|
|
@ -177,6 +177,22 @@ Library loader is extended [loader](#loader) which allows to load published subs
|
|||
</div>
|
||||
</div>
|
||||
|
||||
### Delivery Action ###
|
||||
|
||||
Library Loader contains functionality to export any selected asset, subsets and their version to configurable folder.
|
||||
Delivery follows structure based on defined template, this template must be configured first by Admin in the Settings.
|
||||
|
||||

|
||||
|
||||
* Usage
|
||||
- Select all required subsets for export (you can change theirs versions by double clicking on 'Version' value)
|
||||
- Right click and select **Deliver Versions** from context menu
|
||||
- Select predefined Delivery template (must be configured by Admin system or project wide)
|
||||
- Fill value for root folder (folder will be created if it doesn't exist)
|
||||
- Filter out type of representation you are not interested in
|
||||
- Push **Deliver** button
|
||||
- Dialog must be kept open until export is finished
|
||||
- In a case of problems with any of the representation, that one will be skipped, description of error will be provided in the dialog
|
||||
* * *
|
||||
|
||||
## Publisher
|
||||
|
|
|
|||
BIN
website/docs/assets/tools/tools_delivery_loader.png
Normal file
BIN
website/docs/assets/tools/tools_delivery_loader.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -7,14 +7,31 @@ sidebar_label: Build
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Introduction
|
||||
|
||||
To build Pype you currently need (on all platforms):
|
||||
|
||||
- **[Python 3.7](https://www.python.org/downloads/)** as we are following [vfx platform](https://vfxplatform.com).
|
||||
- **[git](https://git-scm.com/downloads)**
|
||||
|
||||
We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies.
|
||||
We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies and
|
||||
[Poetry](https://python-poetry.org/) for virtual environment management.
|
||||
|
||||
This is outline of build steps. Most of them are done automatically via scripts:
|
||||
- Virtual environment is created using **Poetry** in `.venv`
|
||||
- Necessary third-party tools (like [ffmpeg](https://www.ffmpeg.org/), [OpenImageIO](https://github.com/OpenImageIO/oiio)
|
||||
and [usd libraries](https://developer.nvidia.com/usd)) are downloaded to `./vendor/bin`
|
||||
- OpenPype code is frozen with **cx_freeze** to `./build`
|
||||
- Modules are moved from `lib` to `dependencies` to solve some Python 2 / Python 3 clashes
|
||||
- On Mac application bundle and dmg image will be created from built code.
|
||||
- On Windows, you can create executable installer with `./tools/build_win_installer.ps1`
|
||||
|
||||
### Clone OpenPype repository:
|
||||
```powershell
|
||||
git clone --recurse-submodules https://github.com/pypeclub/OpenPype.git
|
||||
```
|
||||
|
||||
## Platform specific steps
|
||||
|
||||
<Tabs
|
||||
groupId="platforms"
|
||||
|
|
@ -27,32 +44,31 @@ We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the cod
|
|||
|
||||
<TabItem value="win">
|
||||
|
||||
### Windows
|
||||
More tools might be needed for installing some dependencies (for example for **OpenTimelineIO**) - mostly
|
||||
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
|
||||
|
||||
### Clone repository:
|
||||
```sh
|
||||
git clone --recurse-submodules git@github.com:pypeclub/pype.git
|
||||
```
|
||||
|
||||
### Run from source
|
||||
#### Run from source
|
||||
|
||||
For development purposes it is possible to run OpenPype directly from the source. We provide a simple launcher script for this.
|
||||
|
||||
To start OpenPype from source you need to
|
||||
|
||||
1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`
|
||||
2) Run `.\tools\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray.
|
||||
1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv`
|
||||
2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed.
|
||||
3. Run `.\tools\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray.
|
||||
|
||||
Step 1 and 2 needs to be run only once (or when something was changed).
|
||||
|
||||
#### To build OpenPype:
|
||||
1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv`
|
||||
2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed.
|
||||
3. `.\tools\build.ps1` to build OpenPype to `.\build`
|
||||
|
||||
|
||||
### To build OpenPype:
|
||||
|
||||
1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`
|
||||
2) Run `.\tools\build.ps1` to build pype executables in `.\build\`
|
||||
|
||||
To create distributable OpenPype versions, run `./tools/create_zip.ps1` - that will
|
||||
To create distributable OpenPype versions, run `.\tools\create_zip.ps1` - that will
|
||||
create zip file with name `pype-vx.x.x.zip` parsed from current pype repository and
|
||||
copy it to user data dir. You can specify `--path /path/to/zip` to force it into a different
|
||||
copy it to user data dir. You can specify `--path \path\to\zip` to force it into a different
|
||||
location. This can be used to prepare new version releases for artists in the studio environment
|
||||
without the need to re-build the whole package
|
||||
|
||||
|
|
@ -61,27 +77,33 @@ without the need to re-build the whole package
|
|||
</TabItem>
|
||||
<TabItem value="linux">
|
||||
|
||||
### Linux
|
||||
|
||||
#### Docker
|
||||
You can use Docker to build OpenPype. Just run:
|
||||
```sh
|
||||
sudo ./tools/docker_build.sh
|
||||
```shell
|
||||
$ sudo ./tools/docker_build.sh
|
||||
```
|
||||
and you should have built OpenPype in `build` directory. It is using **Centos 7**
|
||||
as a base image.
|
||||
|
||||
You can pull the image:
|
||||
|
||||
```sh
|
||||
```shell
|
||||
# replace 3.0.0 tag with version you want
|
||||
docker pull pypeclub/openpype:3.0.0
|
||||
$ docker pull pypeclub/openpype:3.0.0
|
||||
```
|
||||
See https://hub.docker.com/r/pypeclub/openpype/tag for more.
|
||||
|
||||
Beware that as Python is built against some libraries version in Centos 7 base image,
|
||||
those might not be available in linux version you are using. We try to handle those we
|
||||
found (libffi, libcrypto/ssl, etc.) but there might be more.
|
||||
|
||||
#### Manual build
|
||||
To build OpenPype on Linux you wil need:
|
||||
|
||||
To build OpenPype on Linux you will need:
|
||||
|
||||
- **[curl](https://curl.se)** on systems that doesn't have one preinstalled.
|
||||
- Python header files installed (**python3-dev** on Ubuntu for example).
|
||||
- **bzip2**, **readline**, **sqlite3** and other libraries.
|
||||
|
||||
Because some Linux distros come with newer Python version pre-installed, you might
|
||||
|
|
@ -90,117 +112,283 @@ Your best bet is probably using [pyenv](https://github.com/pyenv/pyenv).
|
|||
|
||||
You can use your package manager to install **git** and other packages to your build
|
||||
environment.
|
||||
Use curl for pyenv installation
|
||||
|
||||
#### Common steps for all Distros
|
||||
|
||||
Use pyenv to prepare Python version for Pype build
|
||||
|
||||
```shell
|
||||
$ curl https://pyenv.run | bash
|
||||
|
||||
# you can add those to ~/.bashrc
|
||||
$ export PATH="$HOME/.pyenv/bin:$PATH"
|
||||
$ eval "$(pyenv init -)"
|
||||
$ eval "$(pyenv virtualenv-init -)"
|
||||
|
||||
# reload shell
|
||||
$ exec $SHELL
|
||||
|
||||
# install Python 3.7.10
|
||||
# python will be downloaded and build so please make sure
|
||||
# you have all necessary requirements installed (see bellow).
|
||||
$ pyenv install -v 3.7.10
|
||||
|
||||
# change path to pype 3
|
||||
$ cd /path/to/pype-3
|
||||
|
||||
# set local python version
|
||||
$ pyenv local 3.7.9
|
||||
```
|
||||
:::note Install build requirements for **Ubuntu**
|
||||
|
||||
```sh
|
||||
```shell
|
||||
sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git
|
||||
```
|
||||
|
||||
In case you run in error about `xcb` when running Pype,
|
||||
you'll need also additional libraries for Qt5:
|
||||
|
||||
```sh
|
||||
```shell
|
||||
sudo apt install qt5-default
|
||||
```
|
||||
:::
|
||||
|
||||
:::note Install build requirements for **Centos**
|
||||
:::note Install build requirements for **Centos 7**
|
||||
|
||||
```sh
|
||||
yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel git
|
||||
```shell
|
||||
$ sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
$ sudo yum install centos-release-scl
|
||||
$ sudo yum install bash which git devtoolset-7-gcc* \
|
||||
make cmake curl wget gcc zlib-devel bzip2 \
|
||||
bzip2-devel readline-devel sqlite sqlite-devel \
|
||||
openssl-devel tk-devel libffi-devel qt5-qtbase-devel \
|
||||
patchelf
|
||||
```
|
||||
|
||||
In case you run in error about `xcb` when running Pype,
|
||||
you'll need also additional libraries for Qt5:
|
||||
|
||||
```sh
|
||||
sudo yum install qt5-qtbase-devel
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
For more information about setting your build environmet please refer to [pyenv suggested build environment](https://github.com/pyenv/pyenv/wiki#suggested-build-environment)
|
||||
:::note Install build requirements for other distros
|
||||
|
||||
#### Common steps for all Distros
|
||||
Build process usually needs some reasonably recent versions of libraries and tools. You
|
||||
can follow what's needed for Ubuntu and change it for your package manager. Centos 7 steps
|
||||
have additional magic to overcame very old versions.
|
||||
:::
|
||||
|
||||
Use pyenv to prepare Python version for Pype build
|
||||
For more information about setting your build environment please refer to [pyenv suggested build environment](https://github.com/pyenv/pyenv/wiki#suggested-build-environment).
|
||||
|
||||
```sh
|
||||
curl https://pyenv.run | bash
|
||||
|
||||
# you can add those to ~/.bashrc
|
||||
export PATH="$HOME/.pyenv/bin:$PATH"
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
|
||||
# reload shell
|
||||
exec $SHELL
|
||||
|
||||
# install Python 3.7.9
|
||||
pyenv install -v 3.7.9
|
||||
|
||||
# change path to pype 3
|
||||
cd /path/to/pype-3
|
||||
|
||||
# set local python version
|
||||
pyenv local 3.7.9
|
||||
|
||||
```
|
||||
|
||||
#### To build Pype:
|
||||
|
||||
1. Run `.\tools\create_env.sh` to create virtual environment in `.\venv`
|
||||
2. Run `.\tools\build.sh` to build pype executables in `.\build\`
|
||||
1. Run `./tools/create_env.sh` to create virtual environment in `./venv`
|
||||
2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed.
|
||||
3. Run `./tools/build.sh` to build pype executables in `.\build\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mac">
|
||||
|
||||
### MacOS
|
||||
To build pype on MacOS you wil need:
|
||||
|
||||
- **[Homebrew](https://brew.sh)**, Easy way of installing everything necessary is to use.
|
||||
- **[Homebrew](https://brew.sh)** - easy way of installing everything necessary.
|
||||
- **[CMake](https://cmake.org/)** to build some external OpenPype dependencies.
|
||||
- **XCode Command Line Tools** (or some other build system)
|
||||
- **[create-dmg](https://formulae.brew.sh/formula/create-dmg)** to create dmg image from application
|
||||
bundle.
|
||||
|
||||
1) Install **Homebrew**:
|
||||
```sh
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```shell
|
||||
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
2) Install **cmake**:
|
||||
```sh
|
||||
brew install cmake
|
||||
```shell
|
||||
$ brew install cmake
|
||||
```
|
||||
|
||||
3) Install [pyenv](https://github.com/pyenv/pyenv):
|
||||
```sh
|
||||
brew install pyenv
|
||||
echo 'eval "$(pypenv init -)"' >> ~/.zshrc
|
||||
pyenv init
|
||||
exec "$SHELL"
|
||||
PATH=$(pyenv root)/shims:$PATH
|
||||
```shell
|
||||
$ brew install pyenv
|
||||
$ echo 'eval "$(pypenv init -)"' >> ~/.zshrc
|
||||
$ pyenv init
|
||||
$ exec "$SHELL"
|
||||
$ PATH=$(pyenv root)/shims:$PATH
|
||||
```
|
||||
|
||||
4) Pull in required Python version 3.7.x
|
||||
```sh
|
||||
```shell
|
||||
# install Python build dependences
|
||||
brew install openssl readline sqlite3 xz zlib
|
||||
$ brew install openssl readline sqlite3 xz zlib
|
||||
|
||||
# replace with up-to-date 3.7.x version
|
||||
pyenv install 3.7.9
|
||||
$ pyenv install 3.7.9
|
||||
```
|
||||
|
||||
5) Set local Python version
|
||||
```sh
|
||||
```shell
|
||||
# switch to Pype source directory
|
||||
pyenv local 3.7.9
|
||||
$ pyenv local 3.7.9
|
||||
```
|
||||
|
||||
6) Install `create-dmg`
|
||||
```shell
|
||||
$ brew install create-dmg
|
||||
```
|
||||
|
||||
#### To build Pype:
|
||||
|
||||
1. Run `.\tools\create_env.sh` to create virtual environment in `.\venv`
|
||||
2. Run `.\tools\build.sh` to build Pype executables in `.\build\`
|
||||
1. Run `./tools/create_env.sh` to create virtual environment in `./venv`.
|
||||
2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed.
|
||||
3. Run `./tools/build.sh` to build OpenPype Application bundle in `./build/`.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Adding dependencies
|
||||
### Python modules
|
||||
If you are extending OpenPype and you need some new modules not included, you can add them
|
||||
to `pyproject.toml` to `[tool.poetry.dependencies]` section.
|
||||
|
||||
```toml title="/pyproject.toml"
|
||||
[tool.poetry.dependencies]
|
||||
python = "3.7.*"
|
||||
aiohttp = "^3.7"
|
||||
aiohttp_json_rpc = "*" # TVPaint server
|
||||
acre = { git = "https://github.com/pypeclub/acre.git" }
|
||||
opentimelineio = { version = "0.14.0.dev1", source = "openpype" }
|
||||
#...
|
||||
```
|
||||
It is useful to add comment to it so others can see why this was added and where it is used.
|
||||
As you can see you can add git repositories or custom wheels (those must be
|
||||
added to `[[tool.poetry.source]]` section).
|
||||
|
||||
To add something only for specific platform, you can use markers like:
|
||||
```toml title="Install pywin32 only on Windows"
|
||||
pywin32 = { version = "300", markers = "sys_platform == 'win32'" }
|
||||
```
|
||||
|
||||
For more information see [Poetry documentation](https://python-poetry.org/docs/dependency-specification/).
|
||||
|
||||
### Binary dependencies
|
||||
To add some binary tool or something that doesn't fit standard Python distribution methods, you
|
||||
can use [fetch_thirdparty_libs](#fetch_thirdparty_libs) script. It will take things defined in
|
||||
`pyproject.toml` under `[openpype]` section like this:
|
||||
|
||||
```toml title="/pyproject.toml"
|
||||
[openpype]
|
||||
|
||||
[openpype.thirdparty.ffmpeg.windows]
|
||||
url = "https://distribute.openpype.io/thirdparty/ffmpeg-4.4-windows.zip"
|
||||
hash = "dd51ba29d64ee238e7c4c3c7301b19754c3f0ee2e2a729c20a0e2789e72db925"
|
||||
# ...
|
||||
```
|
||||
This defines FFMpeg for Windows. It will be downloaded from specified url, its checksum will
|
||||
be validated (it's sha256) and it will be extracted to `/vendor/bin/ffmpeg/windows` (partly taken
|
||||
from its section name).
|
||||
|
||||
## Script tools
|
||||
(replace extension with the one for your system - `ps1` for windows, `sh` for linux/macos)
|
||||
|
||||
### build
|
||||
This will build OpenPype to `build` directory. If virtual environment is not created yet, it will
|
||||
install [Poetry](https://python-poetry.org/) and using it download and install necessary
|
||||
packages needed for build. It is recommended that you run [fetch_thirdparty_libs](#fetch_thirdparty_libs)
|
||||
to download FFMpeg, OpenImageIO and others that are needed by OpenPype and are copied during the build.
|
||||
|
||||
#### Arguments
|
||||
`--no-submodule-update` - to disable updating submodules. This allows to make custom-builds for testing
|
||||
feature changes in submodules.
|
||||
|
||||
### build_win_installer
|
||||
This will take already existing build in `build` directory and create executable installer using
|
||||
[Inno Setup](https://jrsoftware.org/isinfo.php) and definitions in `./inno_setup.iss`. You need OpenPype
|
||||
build using [build script](#build), Inno Setup installed and in PATH before running this script.
|
||||
|
||||
:::note
|
||||
Windows only
|
||||
:::
|
||||
|
||||
### create_env
|
||||
Script to create virtual environment for build and running OpenPype from sources. It is using
|
||||
[Poetry](https://python-poetry.org/). All dependencies are defined in `pyproject.toml`, resolved by
|
||||
Poetry into `poetry.lock` file and then installed. Running this script without Poetry will download
|
||||
it, install it to `.poetry` and then install virtual environment from `poetry.lock` file. If you want
|
||||
to update packages version, just run `poetry update` or delete lock file.
|
||||
|
||||
#### Arguments
|
||||
`--verbose` - to increase verbosity of Poetry. This can be useful for debugging package conflicts.
|
||||
|
||||
### create_zip
|
||||
Script to create packaged OpenPype version from current sources. This will strip developer stuff and
|
||||
package it into zip that can be used for [auto-updates for studio wide distributions](admin_distribute#automatic-updates), etc.
|
||||
Same as:
|
||||
```shell
|
||||
poetry run python ./tools/create_zip.py
|
||||
```
|
||||
|
||||
### docker_build.sh
|
||||
Script to build OpenPype on [Docker](https://www.docker.com/) enabled systems - usually Linux and Windows
|
||||
with [Docker Desktop](https://docs.docker.com/docker-for-windows/install/)
|
||||
and [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) installed.
|
||||
|
||||
It must be run with administrative privileges - `sudo ./docker_build.sh`.
|
||||
|
||||
It will use **Centos 7** base image to build OpenPype. You'll see your build in `./build` folder.
|
||||
|
||||
### fetch_thirdparty_libs
|
||||
This script will download necessary tools for OpenPype defined in `pyproject.toml` like FFMpeg,
|
||||
OpenImageIO and USD libraries and put them to `./vendor/bin`. Those are then included in build.
|
||||
Running it will overwrite everything on their respective paths.
|
||||
Same as:
|
||||
```shell
|
||||
poetry run python ./tools/fetch_thirdparty_libs.py
|
||||
```
|
||||
|
||||
### make_docs
|
||||
Script will run [sphinx](https://www.sphinx-doc.org/) to build api documentation in html. You
|
||||
should see it then under `./docs/build/html`.
|
||||
|
||||
### run_documentation
|
||||
This will start up [Docusaurus](https://docusaurus.io/) to display OpenPype user documentation.
|
||||
Useful for offline browsing or editing documentation itself. You will need [Node.js](https://nodejs.org/)
|
||||
and [Yarn](https://yarnpkg.com/) to run this script. After executing it, you'll see new
|
||||
browser window with current OpenPype documentation.
|
||||
Same as:
|
||||
```shell
|
||||
cd ./website
|
||||
yarn start
|
||||
```
|
||||
|
||||
### run_mongo
|
||||
Helper script to run local mongoDB server for development and testing. You will need
|
||||
[mongoDB server](https://www.mongodb.com/try/download/community) installed in standard location
|
||||
or in PATH (standard location works only on Windows). It will start by default on port `2707` and
|
||||
it will put its db files to `../mongo_db_data` relative to OpenPype sources.
|
||||
|
||||
### run_project_manager
|
||||
Helper script to start OpenPype Project Manager tool.
|
||||
Same as:
|
||||
```shell
|
||||
poetry run python start.py projectmanager
|
||||
```
|
||||
|
||||
### run_settings
|
||||
Helper script to open OpenPype Settings UI.
|
||||
Same as:
|
||||
```shell
|
||||
poetry run python start.py settings --dev
|
||||
```
|
||||
|
||||
### run_tests
|
||||
Runs OpenPype test suite.
|
||||
|
||||
### run_tray
|
||||
Helper script to run OpenPype Tray.
|
||||
Same as:
|
||||
```shell
|
||||
poetry run python start.py tray
|
||||
```
|
||||
|
||||
### update_submodules
|
||||
Helper script to update OpenPype git submodules.
|
||||
Same as:
|
||||
```shell
|
||||
git submodule update --recursive --remote
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "pype-documentation",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"examples": "docusaurus-examples",
|
||||
"start": "docusaurus start",
|
||||
|
|
@ -13,8 +14,8 @@
|
|||
"docusaurus": "docusaurus"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-alpha.72",
|
||||
"@docusaurus/preset-classic": "2.0.0-alpha.72",
|
||||
"@docusaurus/core": "2.0.0-beta.0",
|
||||
"@docusaurus/preset-classic": "2.0.0-beta.0",
|
||||
"classnames": "^2.2.6",
|
||||
"clsx": "^1.1.1",
|
||||
"react": "^16.10.2",
|
||||
|
|
|
|||
3200
website/yarn.lock
3200
website/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue