Merge branch 'develop' into feature/module_attributes_in_tray_menu
|
|
@ -12,6 +12,8 @@ from pypeapp.lib.mongo import (
|
|||
get_default_components
|
||||
)
|
||||
|
||||
from . import resources
|
||||
|
||||
from .plugin import (
|
||||
Extractor,
|
||||
|
||||
|
|
@ -54,6 +56,8 @@ __all__ = [
|
|||
"compose_url",
|
||||
"get_default_components",
|
||||
|
||||
# Resources
|
||||
"resources",
|
||||
# plugin classes
|
||||
"Extractor",
|
||||
# ordering
|
||||
|
|
|
|||
|
|
@ -51,9 +51,10 @@ def get_ftrack_event_mongo_info():
|
|||
|
||||
if not _used_ftrack_url or components["database"] is None:
|
||||
components["database"] = database_name
|
||||
components["collection"] = collection_name
|
||||
|
||||
uri = compose_url(components)
|
||||
components.pop("collection", None)
|
||||
|
||||
uri = compose_url(**components)
|
||||
|
||||
return uri, components["port"], database_name, collection_name
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib import parse
|
||||
import os
|
||||
import webbrowser
|
||||
import functools
|
||||
import pype
|
||||
import inspect
|
||||
from Qt import QtCore
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
class LoginServerHandler(BaseHTTPRequestHandler):
|
||||
'''Login server handler.'''
|
||||
|
||||
message_filepath = resources.get_resource("ftrack", "sign_in_message.html")
|
||||
|
||||
def __init__(self, login_callback, *args, **kw):
|
||||
'''Initialise handler.'''
|
||||
self.login_callback = login_callback
|
||||
|
|
@ -28,23 +28,21 @@ class LoginServerHandler(BaseHTTPRequestHandler):
|
|||
login_credentials = parse.parse_qs(query)
|
||||
api_user = login_credentials['api_user'][0]
|
||||
api_key = login_credentials['api_key'][0]
|
||||
# get path to resources
|
||||
path_items = os.path.dirname(
|
||||
inspect.getfile(pype)
|
||||
).split(os.path.sep)
|
||||
del path_items[-1]
|
||||
path_items.extend(['res', 'ftrack', 'sign_in_message.html'])
|
||||
message_filepath = os.path.sep.join(path_items)
|
||||
message_file = open(message_filepath, 'r')
|
||||
sign_in_message = message_file.read()
|
||||
message_file.close()
|
||||
|
||||
with open(self.message_filepath, "r") as message_file:
|
||||
sign_in_message = message_file.read()
|
||||
|
||||
# formatting html code for python
|
||||
replacement = [('{', '{{'), ('}', '}}'), ('{{}}', '{}')]
|
||||
for r in (replacement):
|
||||
sign_in_message = sign_in_message.replace(*r)
|
||||
replacements = (
|
||||
("{", "{{"),
|
||||
("}", "}}"),
|
||||
("{{}}", "{}")
|
||||
)
|
||||
for replacement in (replacements):
|
||||
sign_in_message = sign_in_message.replace(*replacement)
|
||||
message = sign_in_message.format(api_user)
|
||||
else:
|
||||
message = '<h1>Failed to sign in</h1>'
|
||||
message = "<h1>Failed to sign in</h1>"
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
|
@ -74,7 +72,6 @@ class LoginServerThread(QtCore.QThread):
|
|||
|
||||
def run(self):
|
||||
'''Listen for events.'''
|
||||
# self._server = BaseHTTPServer.HTTPServer(
|
||||
self._server = HTTPServer(
|
||||
('localhost', 0),
|
||||
functools.partial(
|
||||
|
|
|
|||
|
|
@ -96,7 +96,11 @@ class RestApiServer:
|
|||
port = self.find_port()
|
||||
self.rest_api_thread = RestApiThread(self, port)
|
||||
|
||||
statics_dir = os.path.sep.join([os.environ["PYPE_MODULE_ROOT"], "res"])
|
||||
statics_dir = os.path.join(
|
||||
os.environ["PYPE_MODULE_ROOT"],
|
||||
"pype",
|
||||
"resources"
|
||||
)
|
||||
self.register_statics("/res", statics_dir)
|
||||
os.environ["PYPE_STATICS_SERVER"] = "{}/res".format(
|
||||
os.environ["PYPE_REST_API_URL"]
|
||||
|
|
|
|||
32
pype/plugins/blender/create/create_camera.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Create a camera asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import Creator, lib
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
|
||||
class CreateCamera(Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "cameraMain"
|
||||
label = "Camera"
|
||||
family = "camera"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = pype.hosts.blender.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')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in lib.get_selection():
|
||||
collection.objects.link(obj)
|
||||
|
||||
return collection
|
||||
239
pype/plugins/blender/load/load_camera.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""Load a camera asset in Blender."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
logger = logging.getLogger("pype").getChild("blender").getChild("load_camera")
|
||||
|
||||
|
||||
class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
"""Load a camera from a .blend file.
|
||||
|
||||
Warning:
|
||||
Loading the same asset more then once is not properly supported at the
|
||||
moment.
|
||||
"""
|
||||
|
||||
families = ["camera"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Link Camera"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
|
||||
for obj in objects:
|
||||
bpy.data.cameras.remove(obj.data)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name, actions):
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
camera_container = scene.collection.children[lib_container].make_local()
|
||||
|
||||
objects_list = []
|
||||
|
||||
for obj in camera_container.objects:
|
||||
obj = obj.make_local()
|
||||
obj.data.make_local()
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
if actions[0] is not None:
|
||||
if obj.animation_data is None:
|
||||
obj.animation_data_create()
|
||||
obj.animation_data.action = actions[0]
|
||||
|
||||
if actions[1] is not None:
|
||||
if obj.data.animation_data is None:
|
||||
obj.data.animation_data_create()
|
||||
obj.data.animation_data.action = actions[1]
|
||||
|
||||
objects_list.append(obj)
|
||||
|
||||
camera_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
container_name = pype.hosts.blender.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name, (None, None))
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
camera = objects[0]
|
||||
|
||||
actions = (camera.animation_data.action, camera.data.animation_data.action)
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
objects_list = self._process(
|
||||
str(libpath), lib_container, collection.name, actions)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (avalon-core:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
|
@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor):
|
|||
|
||||
label = "Extract Blend"
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action", "layout"]
|
||||
families = ["model", "camera", "rig", "action", "layout", "animation"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
58
pype/tools/tray/modules_imports.json
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
[
|
||||
{
|
||||
"title": "User settings",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.user",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}, {
|
||||
"title": "Ftrack",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.ftrack.tray",
|
||||
"fromlist": ["pype", "modules", "ftrack"]
|
||||
}, {
|
||||
"title": "Muster",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.muster",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}, {
|
||||
"title": "Avalon",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.avalon_apps",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}, {
|
||||
"title": "Clockify",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.clockify",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}, {
|
||||
"title": "Standalone Publish",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.standalonepublish",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}, {
|
||||
"title": "Logging",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.logging.tray",
|
||||
"fromlist": ["pype", "modules", "logging"]
|
||||
}, {
|
||||
"title": "Idle Manager",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.idle_manager",
|
||||
"fromlist": ["pype","modules"]
|
||||
}, {
|
||||
"title": "Timers Manager",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.timers_manager",
|
||||
"fromlist": ["pype","modules"]
|
||||
}, {
|
||||
"title": "Rest Api",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.rest_api",
|
||||
"fromlist": ["pype","modules"]
|
||||
}, {
|
||||
"title": "Adobe Communicator",
|
||||
"type": "module",
|
||||
"import_path": "pype.modules.adobe_communicator",
|
||||
"fromlist": ["pype", "modules"]
|
||||
}
|
||||
]
|
||||
|
|
@ -12,28 +12,34 @@ class TrayManager:
|
|||
|
||||
Load submenus, actions, separators and modules into tray's context.
|
||||
"""
|
||||
modules = {}
|
||||
services = {}
|
||||
services_submenu = None
|
||||
|
||||
errors = []
|
||||
items = (
|
||||
config.get_presets(first_run=True)
|
||||
.get('tray', {})
|
||||
.get('menu_items', [])
|
||||
)
|
||||
available_sourcetypes = ['python', 'file']
|
||||
available_sourcetypes = ["python", "file"]
|
||||
|
||||
def __init__(self, tray_widget, main_window):
|
||||
self.tray_widget = tray_widget
|
||||
self.main_window = main_window
|
||||
|
||||
self.log = Logger().get_logger(self.__class__.__name__)
|
||||
|
||||
self.icon_run = QtGui.QIcon(get_resource('circle_green.png'))
|
||||
self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png'))
|
||||
self.icon_failed = QtGui.QIcon(get_resource('circle_red.png'))
|
||||
self.modules = {}
|
||||
self.services = {}
|
||||
self.services_submenu = None
|
||||
|
||||
self.services_thread = None
|
||||
self.errors = []
|
||||
|
||||
CURRENT_DIR = os.path.dirname(__file__)
|
||||
self.modules_imports = config.load_json(
|
||||
os.path.join(CURRENT_DIR, "modules_imports.json")
|
||||
)
|
||||
presets = config.get_presets(first_run=True)
|
||||
try:
|
||||
self.modules_usage = presets["tray"]["menu_items"]["item_usage"]
|
||||
except Exception:
|
||||
self.modules_usage = {}
|
||||
self.log.critical("Couldn't find modules usage data.")
|
||||
|
||||
self.icon_run = QtGui.QIcon(get_resource("circle_green.png"))
|
||||
self.icon_stay = QtGui.QIcon(get_resource("circle_orange.png"))
|
||||
self.icon_failed = QtGui.QIcon(get_resource("circle_red.png"))
|
||||
|
||||
def process_presets(self):
|
||||
"""Add modules to tray by presets.
|
||||
|
|
@ -46,21 +52,10 @@ class TrayManager:
|
|||
"item_usage": {
|
||||
"Statics Server": false
|
||||
}
|
||||
}, {
|
||||
"item_import": [{
|
||||
"title": "Ftrack",
|
||||
"type": "module",
|
||||
"import_path": "pype.ftrack.tray",
|
||||
"fromlist": ["pype", "ftrack"]
|
||||
}, {
|
||||
"title": "Statics Server",
|
||||
"type": "module",
|
||||
"import_path": "pype.services.statics_server",
|
||||
"fromlist": ["pype","services"]
|
||||
}]
|
||||
}
|
||||
In this case `Statics Server` won't be used.
|
||||
"""
|
||||
|
||||
# Backwards compatible presets loading
|
||||
if isinstance(self.items, list):
|
||||
items = self.items
|
||||
|
|
|
|||