mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #1095 from pypeclub/3.0/feature/643-mark-publishable-instances-in-photoshop
Feature/643 mark publishable instances in photoshop
This commit is contained in:
commit
e6d60d9a67
6 changed files with 217 additions and 36 deletions
|
|
@ -73,5 +73,17 @@ class CreateImage(pype.api.Creator):
|
|||
groups.append(group)
|
||||
|
||||
for group in groups:
|
||||
long_names = []
|
||||
if group.long_name:
|
||||
for directory in group.long_name[::-1]:
|
||||
name = directory.replace(stub.PUBLISH_ICON, '').\
|
||||
replace(stub.LOADED_ICON, '')
|
||||
long_names.append(name)
|
||||
|
||||
self.data.update({"subset": "image" + group.name})
|
||||
self.data.update({"uuid": str(group.id)})
|
||||
self.data.update({"long_name": "_".join(long_names)})
|
||||
stub.imprint(group, self.data)
|
||||
# reusing existing group, need to rename afterwards
|
||||
if not create_group:
|
||||
stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
stub = photoshop.stub()
|
||||
layers = stub.get_layers()
|
||||
layers_meta = stub.get_layers_metadata()
|
||||
instance_names = []
|
||||
for layer in layers:
|
||||
layer_data = stub.read(layer, layers_meta)
|
||||
|
||||
|
|
@ -41,14 +42,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
# self.log.info("%s skipped, it was empty." % layer.Name)
|
||||
# continue
|
||||
|
||||
instance = context.create_instance(layer.name)
|
||||
instance = context.create_instance(layer_data["subset"])
|
||||
instance.append(layer)
|
||||
instance.data.update(layer_data)
|
||||
instance.data["families"] = self.families_mapping[
|
||||
layer_data["family"]
|
||||
]
|
||||
instance.data["publish"] = layer.visible
|
||||
instance_names.append(layer_data["subset"])
|
||||
|
||||
# Produce diagnostic message for any graphical
|
||||
# user interface interested in visualising it.
|
||||
self.log.info("Found: \"%s\" " % instance.data["name"])
|
||||
self.log.info("instance: {} ".format(instance.data))
|
||||
|
||||
if len(instance_names) != len(set(instance_names)):
|
||||
self.log.warning("Duplicate instances found. " +
|
||||
"Remove unwanted via SubsetManager")
|
||||
|
|
|
|||
|
|
@ -25,11 +25,15 @@ class ValidateNamingRepair(pyblish.api.Action):
|
|||
for instance in instances:
|
||||
self.log.info("validate_naming instance {}".format(instance))
|
||||
name = instance.data["name"].replace(" ", "_")
|
||||
name = name.replace(instance.data["family"], '')
|
||||
instance[0].Name = name
|
||||
data = stub.read(instance[0])
|
||||
data["subset"] = "image" + name
|
||||
stub.imprint(instance[0], data)
|
||||
|
||||
name = stub.PUBLISH_ICON + name
|
||||
stub.rename_layer(instance.data["uuid"], name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -46,8 +50,11 @@ class ValidateNaming(pyblish.api.InstancePlugin):
|
|||
actions = [ValidateNamingRepair]
|
||||
|
||||
def process(self, instance):
|
||||
msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
|
||||
help_msg = ' Use Repair action (A) in Pyblish to fix it.'
|
||||
msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"],
|
||||
help_msg)
|
||||
assert " " not in instance.data["name"], msg
|
||||
|
||||
msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"])
|
||||
msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"],
|
||||
help_msg)
|
||||
assert " " not in instance.data["subset"], msg
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import pyblish.api
|
||||
import pype.api
|
||||
|
||||
|
||||
class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
Validate that all subset's names are unique.
|
||||
"""
|
||||
|
||||
label = "Validate Subset Uniqueness"
|
||||
hosts = ["photoshop"]
|
||||
order = pype.api.ValidateContentsOrder
|
||||
families = ["image"]
|
||||
|
||||
def process(self, context):
|
||||
subset_names = []
|
||||
|
||||
for instance in context:
|
||||
if instance.data.get('publish'):
|
||||
subset_names.append(instance.data.get('subset'))
|
||||
|
||||
msg = (
|
||||
"Instance subset names are not unique. " +
|
||||
"Remove duplicates via SubsetManager."
|
||||
)
|
||||
assert len(subset_names) == len(set(subset_names)), msg
|
||||
|
|
@ -54,6 +54,9 @@ class Photoshop(WebSocketRoute):
|
|||
async def projectmanager_route(self):
|
||||
self._tool_route("projectmanager")
|
||||
|
||||
async def subsetmanager_route(self):
|
||||
self._tool_route("subsetmanager")
|
||||
|
||||
def _tool_route(self, tool_name):
|
||||
"""The address accessed when clicking on the buttons."""
|
||||
partial_method = functools.partial(photoshop.show, tool_name)
|
||||
|
|
|
|||
|
|
@ -4,16 +4,37 @@ from pype.modules.websocket_server import WebSocketServer
|
|||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import json
|
||||
from collections import namedtuple
|
||||
import attr
|
||||
|
||||
|
||||
class PhotoshopServerStub():
|
||||
@attr.s
|
||||
class PSItem(object):
|
||||
"""
|
||||
Object denoting layer or group item in PS. Each item is created in
|
||||
PS by any Loader, but contains same fields, which are being used
|
||||
in later processing.
|
||||
"""
|
||||
# metadata
|
||||
id = attr.ib() # id created by AE, could be used for querying
|
||||
name = attr.ib() # name of item
|
||||
group = attr.ib(default=None) # item type (footage, folder, comp)
|
||||
parents = attr.ib(factory=list)
|
||||
visible = attr.ib(default=True)
|
||||
type = attr.ib(default=None)
|
||||
# all imported elements, single for
|
||||
members = attr.ib(factory=list)
|
||||
long_name = attr.ib(default=None)
|
||||
|
||||
|
||||
class PhotoshopServerStub:
|
||||
"""
|
||||
Stub for calling function on client (Photoshop js) side.
|
||||
Expects that client is already connected (started when avalon menu
|
||||
is opened).
|
||||
'self.websocketserver.call' is used as async wrapper
|
||||
"""
|
||||
PUBLISH_ICON = '\u2117 '
|
||||
LOADED_ICON = '\u25bc'
|
||||
|
||||
def __init__(self):
|
||||
self.websocketserver = WebSocketServer.get_instance()
|
||||
|
|
@ -34,7 +55,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Parses layer metadata from Headline field of active document
|
||||
Args:
|
||||
layer: <namedTuple Layer("id":XX, "name":"YYY")
|
||||
layer: (PSItem)
|
||||
layers_meta: full list from Headline (for performance in loops)
|
||||
Returns:
|
||||
"""
|
||||
|
|
@ -46,10 +67,33 @@ class PhotoshopServerStub():
|
|||
def imprint(self, layer, data, all_layers=None, layers_meta=None):
|
||||
"""
|
||||
Save layer metadata to Headline field of active document
|
||||
|
||||
Stores metadata in format:
|
||||
[{
|
||||
"active":true,
|
||||
"subset":"imageBG",
|
||||
"family":"image",
|
||||
"id":"pyblish.avalon.instance",
|
||||
"asset":"Town",
|
||||
"uuid": "8"
|
||||
}] - for created instances
|
||||
OR
|
||||
[{
|
||||
"schema": "avalon-core:container-2.0",
|
||||
"id": "pyblish.avalon.instance",
|
||||
"name": "imageMG",
|
||||
"namespace": "Jungle_imageMG_001",
|
||||
"loader": "ImageLoader",
|
||||
"representation": "5fbfc0ee30a946093c6ff18a",
|
||||
"members": [
|
||||
"40"
|
||||
]
|
||||
}] - for loaded instances
|
||||
|
||||
Args:
|
||||
layer (namedtuple): Layer("id": XXX, "name":'YYY')
|
||||
layer (PSItem):
|
||||
data(string): json representation for single layer
|
||||
all_layers (list of namedtuples): for performance, could be
|
||||
all_layers (list of PSItem): for performance, could be
|
||||
injected for usage in loop, if not, single call will be
|
||||
triggered
|
||||
layers_meta(string): json representation from Headline
|
||||
|
|
@ -59,6 +103,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
if not layers_meta:
|
||||
layers_meta = self.get_layers_metadata()
|
||||
|
||||
# json.dumps writes integer values in a dictionary to string, so
|
||||
# anticipating it here.
|
||||
if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
|
||||
|
|
@ -73,11 +118,11 @@ class PhotoshopServerStub():
|
|||
if not all_layers:
|
||||
all_layers = self.get_layers()
|
||||
layer_ids = [layer.id for layer in all_layers]
|
||||
cleaned_data = {}
|
||||
cleaned_data = []
|
||||
|
||||
for id in layers_meta:
|
||||
if int(id) in layer_ids:
|
||||
cleaned_data[id] = layers_meta[id]
|
||||
cleaned_data.append(layers_meta[id])
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
|
|
@ -89,7 +134,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Returns JSON document with all(?) layers in active document.
|
||||
|
||||
Returns: <list of namedtuples>
|
||||
Returns: <list of PSItem>
|
||||
Format of tuple: { 'id':'123',
|
||||
'name': 'My Layer 1',
|
||||
'type': 'GUIDE'|'FG'|'BG'|'OBJ'
|
||||
|
|
@ -100,12 +145,26 @@ class PhotoshopServerStub():
|
|||
|
||||
return self._to_records(res)
|
||||
|
||||
def get_layer(self, layer_id):
|
||||
"""
|
||||
Returns PSItem for specific 'layer_id' or None if not found
|
||||
Args:
|
||||
layer_id (string): unique layer id, stored in 'uuid' field
|
||||
|
||||
Returns:
|
||||
(PSItem) or None
|
||||
"""
|
||||
layers = self.get_layers()
|
||||
for layer in layers:
|
||||
if str(layer.id) == str(layer_id):
|
||||
return layer
|
||||
|
||||
def get_layers_in_layers(self, layers):
|
||||
"""
|
||||
Return all layers that belong to layers (might be groups).
|
||||
Args:
|
||||
layers <list of namedTuples>:
|
||||
Returns: <list of namedTuples>
|
||||
layers <list of PSItem>:
|
||||
Returns: <list of PSItem>
|
||||
"""
|
||||
all_layers = self.get_layers()
|
||||
ret = []
|
||||
|
|
@ -123,28 +182,30 @@ class PhotoshopServerStub():
|
|||
def create_group(self, name):
|
||||
"""
|
||||
Create new group (eg. LayerSet)
|
||||
Returns: <namedTuple Layer("id":XX, "name":"YYY")>
|
||||
Returns: <PSItem>
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
ret = self.websocketserver.call(self.client.call
|
||||
('Photoshop.create_group',
|
||||
name=name))
|
||||
name=enhanced_name))
|
||||
# create group on PS is asynchronous, returns only id
|
||||
layer = {"id": ret, "name": name, "group": True}
|
||||
return namedtuple('Layer', layer.keys())(*layer.values())
|
||||
return PSItem(id=ret, name=name, group=True)
|
||||
|
||||
def group_selected_layers(self, name):
|
||||
"""
|
||||
Group selected layers into new LayerSet (eg. group)
|
||||
Returns: (Layer)
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.group_selected_layers',
|
||||
name=name)
|
||||
name=enhanced_name)
|
||||
)
|
||||
res = self._to_records(res)
|
||||
|
||||
if res:
|
||||
return res.pop()
|
||||
rec = res.pop()
|
||||
rec.name = rec.name.replace(self.PUBLISH_ICON, '')
|
||||
return rec
|
||||
raise ValueError("No group record returned")
|
||||
|
||||
def get_selected_layers(self):
|
||||
|
|
@ -163,11 +224,10 @@ class PhotoshopServerStub():
|
|||
layers: <list of Layer('id':XX, 'name':"YYY")>
|
||||
Returns: None
|
||||
"""
|
||||
layer_ids = [layer.id for layer in layers]
|
||||
|
||||
layers_id = [str(lay.id) for lay in layers]
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_layers',
|
||||
layers=layer_ids)
|
||||
('Photoshop.select_layers',
|
||||
layers=json.dumps(layers_id))
|
||||
)
|
||||
|
||||
def get_active_document_full_name(self):
|
||||
|
|
@ -238,7 +298,14 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Reads layers metadata from Headline from active document in PS.
|
||||
(Headline accessible by File > File Info)
|
||||
Returns(string): - json documents
|
||||
|
||||
Returns:
|
||||
(string): - json documents
|
||||
example:
|
||||
{"8":{"active":true,"subset":"imageBG",
|
||||
"family":"image","id":"pyblish.avalon.instance",
|
||||
"asset":"Town"}}
|
||||
8 is layer(group) id - used for deletion, update etc.
|
||||
"""
|
||||
layers_data = {}
|
||||
res = self.websocketserver.call(self.client.call('Photoshop.read'))
|
||||
|
|
@ -246,6 +313,23 @@ class PhotoshopServerStub():
|
|||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
# format of metadata changed from {} to [] because of standardization
|
||||
# keep current implementation logic as its working
|
||||
if not isinstance(layers_data, dict):
|
||||
temp_layers_meta = {}
|
||||
for layer_meta in layers_data:
|
||||
layer_id = layer_meta.get("uuid") or \
|
||||
(layer_meta.get("members")[0])
|
||||
temp_layers_meta[layer_id] = layer_meta
|
||||
layers_data = temp_layers_meta
|
||||
else:
|
||||
# legacy version of metadata
|
||||
for layer_id, layer_meta in layers_data.items():
|
||||
if layer_meta.get("schema") != "avalon-core:container-2.0":
|
||||
layer_meta["uuid"] = str(layer_id)
|
||||
else:
|
||||
layer_meta["members"] = [str(layer_id)]
|
||||
|
||||
return layers_data
|
||||
|
||||
def import_smart_object(self, path, layer_name):
|
||||
|
|
@ -257,11 +341,14 @@ class PhotoshopServerStub():
|
|||
layer_name (str): Unique layer name to differentiate how many times
|
||||
same smart object was loaded
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.import_smart_object',
|
||||
path=path, name=layer_name))
|
||||
|
||||
return self._to_records(res).pop()
|
||||
path=path, name=enhanced_name))
|
||||
rec = self._to_records(res).pop()
|
||||
if rec:
|
||||
rec.name = rec.name.replace(self.LOADED_ICON, '')
|
||||
return rec
|
||||
|
||||
def replace_smart_object(self, layer, path, layer_name):
|
||||
"""
|
||||
|
|
@ -270,13 +357,14 @@ class PhotoshopServerStub():
|
|||
same smart object was loaded
|
||||
|
||||
Args:
|
||||
layer (namedTuple): Layer("id":XX, "name":"YY"..).
|
||||
layer (PSItem):
|
||||
path (str): File to import.
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.replace_smart_object',
|
||||
layer_id=layer.id,
|
||||
path=path, name=layer_name))
|
||||
path=path, name=enhanced_name))
|
||||
|
||||
def delete_layer(self, layer_id):
|
||||
"""
|
||||
|
|
@ -288,24 +376,62 @@ class PhotoshopServerStub():
|
|||
('Photoshop.delete_layer',
|
||||
layer_id=layer_id))
|
||||
|
||||
def rename_layer(self, layer_id, name):
|
||||
"""
|
||||
Renames specific layer by it's id.
|
||||
Args:
|
||||
layer_id (int): id of layer to delete
|
||||
name (str): new name
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.rename_layer',
|
||||
layer_id=layer_id,
|
||||
name=name))
|
||||
|
||||
def remove_instance(self, instance_id):
|
||||
cleaned_data = {}
|
||||
|
||||
for key, instance in self.get_layers_metadata().items():
|
||||
if key != instance_id:
|
||||
cleaned_data[key] = instance
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.imprint', payload=payload)
|
||||
)
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
|
||||
def _to_records(self, res):
|
||||
"""
|
||||
Converts string json representation into list of named tuples for
|
||||
Converts string json representation into list of PSItem for
|
||||
dot notation access to work.
|
||||
Returns: <list of named tuples>
|
||||
res(string): - json representation
|
||||
Args:
|
||||
res (string): valid json
|
||||
Returns:
|
||||
<list of PSItem>
|
||||
"""
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(res))
|
||||
ret = []
|
||||
# convert to namedtuple to use dot donation
|
||||
if isinstance(layers_data, dict): # TODO refactore
|
||||
|
||||
# convert to AEItem to use dot donation
|
||||
if isinstance(layers_data, dict):
|
||||
layers_data = [layers_data]
|
||||
for d in layers_data:
|
||||
ret.append(namedtuple('Layer', d.keys())(*d.values()))
|
||||
# currently implemented and expected fields
|
||||
item = PSItem(d.get('id'),
|
||||
d.get('name'),
|
||||
d.get('group'),
|
||||
d.get('parents'),
|
||||
d.get('visible'),
|
||||
d.get('type'),
|
||||
d.get('members'),
|
||||
d.get('long_name'))
|
||||
|
||||
ret.append(item)
|
||||
return ret
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue