diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..4e1f3d3c14 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.js eol=lf +*.c eol=lf diff --git a/pype/hosts/harmony/js/.eslintrc.json b/pype/hosts/harmony/js/.eslintrc.json index f88e9716b4..ffbcae87f4 100644 --- a/pype/hosts/harmony/js/.eslintrc.json +++ b/pype/hosts/harmony/js/.eslintrc.json @@ -1,15 +1,117 @@ { "env": { - "browser": true + "browser": true, + "es2021": true }, - "extends": [ - "google" - ], + "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 3 }, "rules": { - "no-var": "off", - "comma-dangle": "off" + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + }, + "globals": { + "Action": "readonly", + "Backdrop": "readonly", + "Button": "readonly", + "Cel": "readonly", + "Cel3d": "readonly", + "CheckBox": "readonly", + "ColorRGBA": "readonly", + "ComboBox": "readonly", + "DateEdit": "readonly", + "DateEditEnum": "readonly", + "Dialog": "readonly", + "Dir": "readonly", + "DirSpec": "readonly", + "Drawing": "readonly", + "DrawingToolParams": "readonly", + "DrawingTools": "readonly", + "EnvelopeCreator": "readonly", + "ExportVideoDlg": "readonly", + "File": "readonly", + "FileAccess": "readonly", + "FileDialog": "readonly", + "GroupBox": "readonly", + "ImportDrawingDlg": "readonly", + "Input": "readonly", + "KeyModifiers": "readonly", + "Label": "readonly", + "LayoutExports": "readonly", + "LayoutExportsParams": "readonly", + "LineEdit": "readonly", + "Matrix4x4": "readonly", + "MessageBox": "readonly", + "MessageLog": "readonly", + "Model3d": "readonly", + "MovieImport": "readonly", + "NumberEdit": "readonly", + "PaletteManager": "readonly", + "PaletteObjectManager": "readonly", + "PermanentFile": "readonly", + "Point2d": "readonly", + "Point3d": "readonly", + "Process": "readonly", + "Process2": "readonly", + "Quaternion": "readonly", + "QuicktimeExporter": "readonly", + "RadioButton": "readonly", + "RemoteCmd": "readonly", + "Scene": "readonly", + "Settings": "readonly", + "Slider": "readonly", + "SpinBox": "readonly", + "SubnodeData": "readonly", + "System": "readonly", + "TemporaryFile": "readonly", + "TextEdit": "readonly", + "TimeEdit": "readonly", + "Timeline": "readonly", + "ToolProperties": "readonly", + "UiLoader": "readonly", + "Vector2d": "readonly", + "Vector3d": "readonly", + "WebCCExporter": "readonly", + "Workspaces": "readonly", + "__scriptManager__": "readonly", + "__temporaryFileContext__": "readonly", + "about": "readonly", + "column": "readonly", + "compositionOrder": "readonly", + "copyPaste": "readonly", + "deformation": "readonly", + "drawingExport": "readonly", + "element": "readonly", + "exporter": "readonly", + "fileMapper": "readonly", + "frame": "readonly", + "func": "readonly", + "library": "readonly", + "node": "readonly", + "preferences": "readonly", + "render": "readonly", + "scene": "readonly", + "selection": "readonly", + "sound": "readonly", + "specialFolders": "readonly", + "translator": "readonly", + "view": "readonly", + "waypoint": "readonly", + "xsheet": "readonly" } } diff --git a/pype/hosts/harmony/js/pype_harmony.js b/pype/hosts/harmony/js/PypeHarmony.js similarity index 95% rename from pype/hosts/harmony/js/pype_harmony.js rename to pype/hosts/harmony/js/PypeHarmony.js index c8de8063c0..740796feef 100644 --- a/pype/hosts/harmony/js/pype_harmony.js +++ b/pype/hosts/harmony/js/PypeHarmony.js @@ -63,11 +63,12 @@ PypeHarmony.setSceneSettings = function(settings) { * Set color of nodes. * @function * @param {array} nodes List of nodes. + * @param {array} rgba array of RGBA components of color. */ -PypeHarmony.setColor = function(nodes) { +PypeHarmony.setColor = function(nodes, rgba) { for (var i =0; i <= nodes.length - 1; ++i) { - var redColor = new ColorRGBA(255, 0, 0, 255); - node.setColor(nodes[i], redColor); + var color = PypeHarmony.color(rgba); + node.setColor(nodes[i], color); } }; @@ -147,6 +148,7 @@ PypeHarmony.copyFile = function(src, dst) { srcFile.copy(dstFile); }; + /** * create RGBA color from array. * @function diff --git a/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js b/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js index 3aa0648a4f..090a401344 100644 --- a/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js +++ b/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js @@ -13,9 +13,6 @@ if (typeof PypeHarmony !== 'undefined') { /** * @namespace * @classdesc Image Sequence loader JS code. - * @property {Object} loaders Namespace for Loaders JS code. - * @property {Object} Creators Namespace for Creators JS code. - * @property {Object} Publish Namespace for Publish plugins JS code. */ ImageSequenceLoader = function() { this.PNGTransparencyMode = 0; // Premultiplied wih Black @@ -57,17 +54,20 @@ ImageSequenceLoader.prototype.getUniqueColumnName = function(columnPrefix) { * @example * // Agrguments are in following order: * var args = [ - * root, // Harmony root - * files, // Files in file sequences - * name, // Node name - * startFrame // Sequence starting frame + * files, // Files in file sequences. + * asset, // Asset name. + * subset, // Subset name. + * startFrame, // Sequence starting frame. + * groupId // Unique group ID (uuid4). * ]; */ ImageSequenceLoader.prototype.importFiles = function(args) { - var root = args[0]; - var files = args[1]; - var name = args[2]; + var doc = $.scn; + var files = args[0]; + var asset = args[1]; + var subset = args[2]; var startFrame = args[3]; + var groupId = args[4]; var vectorFormat = null; var extension = null; var filename = files[0]; @@ -76,6 +76,29 @@ ImageSequenceLoader.prototype.importFiles = function(args) { return null; } + // Get the current group + nodeViewWidget = $.app.getWidgetByName('Node View'); + if (!nodeViewWidget) { + $.alert('You must have a Node View open!', 'No Node View!', 'OK!'); + return; + } + + nodeViewWidget.setFocus(); + var nodeView = view.currentView(); + var currentGroup = null; + if (!nodeView) { + currentGroup = doc.root; + } else { + currentGroup = doc.$node(view.group(nodeView)); + } + // Get a unique iterative name for the container read node + var num = 0; + var name = ''; + do { + name = asset + '_' + (num++) + '_' + subset; + } while (current_group.getNodeByName(name) != null); + + extension = filename.substr(pos+1).toLowerCase(); if (extension == 'jpeg') { extension = 'jpg'; @@ -102,7 +125,7 @@ ImageSequenceLoader.prototype.importFiles = function(args) { var uniqueColumnName = this.getUniqueColumnName(name); column.add(uniqueColumnName, 'DRAWING'); column.setElementIdOfDrawing(uniqueColumnName, elemId); - var read = node.add(root, name, 'READ', 0, 0, 0); + var read = node.add(currentGroup, name, 'READ', 0, 0, 0); var transparencyAttr = node.getAttr( read, frame.current(), 'READ_TRANSPARENCY' ); @@ -156,6 +179,10 @@ ImageSequenceLoader.prototype.importFiles = function(args) { } var greenColor = new ColorRGBA(0, 255, 0, 255); node.setColor(read, greenColor); + + // Add uuid to attribute of the container read node + node.createDynamicAttr(read, 'STRING', 'uuid', 'uuid', false); + node.setTextAttr(read, 'uuid', 1.0, groupId); return read; }; diff --git a/pype/hosts/harmony/js/loaders/TemplateLoader.js b/pype/hosts/harmony/js/loaders/TemplateLoader.js new file mode 100644 index 0000000000..f9fe24e187 --- /dev/null +++ b/pype/hosts/harmony/js/loaders/TemplateLoader.js @@ -0,0 +1,175 @@ +// *************************************************************************** +// * TemplateLoader * +// *************************************************************************** + + +// check if PypeHarmony is defined and if not, load it. +if (typeof PypeHarmony !== 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS'); + include(PYPE_HARMONY_JS + '/pype_harmony.js'); +} + + +/** + * @namespace + * @classdesc Image Sequence loader JS code. + */ +TemplateLoader = function() {}; + + +/** + * Load template as container. + * @function + * @param {array} args Arguments, see example. + * @return {string} Name of container. + * + * @example + * // arguments are in following order: + * var args = [ + * templatePath, // Path to tpl file. + * assetName, // Asset name. + * subsetName, // Subset name. + * groupId // unique ID (uuid4) + * ]; + */ +TemplateLoader.prototype.loadContainer = function(args) { + var doc = $.scn; + var templatePath = args[0]; + var assetName = args[1]; + var subset = args[2]; + var groupId = args[3]; + + // Get the current group + nodeViewWidget = $.app.getWidgetByName('Node View'); + if (!nodeViewWidget) { + $.alert('You must have a Node View open!', 'No Node View!', 'OK!'); + return; + } + + nodeViewWidget.setFocus(); + nodeView = view.currentView(); + if (!nodeView) { + currentGroup = doc.root; + } else { + currentGroup = doc.$node(view.group(nodeView)); + } + + // Get a unique iterative name for the container group + var num = 0; + var containerGroupName = ''; + do { + containerGroupName = assetName + '_' + (num++) + '_' + subset; + } while (currentGroup.getNodeByName(containerGroupName) != null); + + // import the template + var tplNodes = currentGroup.importTemplate(templatePath); + MessageLog.trace(tplNodes); + // Create the container group + var groupNode = currentGroup.addGroup( + containerGroupName, false, false, tplNodes); + + // Add uuid to attribute of the container group + node.createDynamicAttr(groupNode, 'STRING', 'uuid', 'uuid', false); + node.setTextAttr(groupNode, 'uuid', 1.0, groupId); + + return String(groupNode); +}; + + +/** + * Replace existing node container. + * @function + * @param {string} dstNodePath Harmony path to destination Node. + * @param {string} srcNodePath Harmony path to source Node. + * @param {string} renameSrc ... + * @param {boolean} cloneSrc ... + * @param {array} linkColumns ... + * @return {boolean} Success + * @todo This is work in progress. + */ +TemplateLoader.prototype.replaceNode = function( + dstNodePath, srcNodePath, renameSrc, cloneSrc, linkColumns) { + var doc = $.scn; + var srcNode = doc.$node(srcNodePath); + var dstNode = doc.$node(dstNodePath); + // var dstNodeName = dstNode.name; + var replacementNode = srcNode; + // var dstGroup = dstNode.group; + $.beginUndo(); + if (cloneSrc) { + var replacementNode = doc.$node( + Y.nodeTools.copy_paste_node( + srcNodePath, dstNode.name + '_CLONE', dstNode.group.path)); + } else { + if (replacement_node.group.path != src_node.group.path) { + replacement_node.moveToGroup(dst_group); + } + } + var inLinks = dstNode.getInLinks(); + for (l in inLinks) { + if (Object.prototype.hasOwnProperty.call(inLinks, l)) { + var link = inLinks[l]; + inPort = Number(link.inPort); + outPort = Number(link.outPort); + outNode = link.outNode; + success = replacement_node.linkInNode(outNode, inPort, outPort); + if (success) { + log('Successfully connected ' + outNode + ' : ' + + outPort + ' -> ' + replacementNode + ' : ' + inPort); + } else { + log('Failed to connect ' + outNode + ' : ' + + outPort + ' -> ' + replacementNode + ' : ' + inPort); + } + } + } + + var outLinks = dstNode.getOutLinks(); + for (l in outLinks) { + if (Object.prototype.hasOwnProperty.call(outLinks, l)) { + var link = out_links[l]; + inPort = Number(link.inPort); + outPort = Number(link.outPort); + inNode = link.inNode; + // first we must disconnect the port from the node being + // replaced to this links inNode port + inNode.unlinkInPort(inPort); + success = replacement_node.linkOutNode(in_node, out_port, in_port); + if (success) { + log('Successfully connected ' + inNode + ' : ' + + inPort + ' <- ' + replacementNode + ' : ' + outPort); + } else { + if (in_node.type == 'MultiLayerWrite') { + log('Attempting standard api to connect the nodes...'); + success = node.link( + replacementNode, outPort, inNode, + inPort, node.numberOfInputPorts(inNode) + 1); + if (success) { + log('Successfully connected ' + inNode + ' : ' + + inPort + ' <- ' + replacementNode + ' : ' + outPort); + } + } + } + if (!success) { + log('Failed to connect ' + inNode + ' : ' + + inPort + ' <- ' + replacementNode + ' : ' + outPort); + return false; + } + } + } +}; + + +TemplateLoader.prototype.askForColumnsUpdate = function() { + // Ask user if they want to also update columns and + // linked attributes here + return ($.confirm( + 'Would you like to update in place and reconnect all \n' + + 'ins/outs, attributes, and columns?', + 'Update & Replace?\n' + + 'If you choose No, the version will only be loaded.', + 'Yes', + 'No')); +}; + +// add self to Pype Loaders +PypeHarmony.Loaders.TemplateLoader = new TemplateLoader(); diff --git a/pype/hosts/harmony/js/loaders/package.json b/pype/hosts/harmony/js/loaders/package.json new file mode 100644 index 0000000000..b4b89c8050 --- /dev/null +++ b/pype/hosts/harmony/js/loaders/package.json @@ -0,0 +1,20 @@ +{ + "name": "pype-harmony", + "version": "1.0.0", + "description": "Pype Harmony Host integration", + "main": "PypeHarmony.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Pype", + "Avalon", + "Harmony", + "pipeline" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "eslint": "^7.11.0" + } +} diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 7809d890f5..b6f4845983 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -2,6 +2,7 @@ """Loader for image sequences.""" import os import uuid +from pathlib import Path import clique @@ -28,36 +29,37 @@ class ImageSequenceLoader(api.Loader): data (dict, optional): Additional data passed into loader. """ + fname = Path(self.fname) self_name = self.__class__.__name__ collections, remainder = clique.assemble( - os.listdir(os.path.dirname(self.fname)) + os.listdir(fname.parent.as_posix()) ) files = [] if collections: for f in list(collections[0]): - files.append( - os.path.join( - os.path.dirname(self.fname), f - ).replace("\\", "/") - ) + files.append(fname.parent.joinpath(f).as_posix()) else: - files.append( - os.path.join( - os.path.dirname(self.fname), remainder[0] - ).replace("\\", "/") - ) + files.append(fname.parent.joinpath(remainder[0]).as_posix()) - name = context["subset"]["name"] - name += "_{}".format(uuid.uuid4()) + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + group_id = str(uuid.uuid4()) read_node = harmony.send( { "function": f"PypeHarmony.Loaders.{self_name}.importFiles", # noqa: E501 - "args": ["Top", files, name, 1] + "args": [ + files, + asset, + subset, + 1, + group_id + ] } )["result"] return harmony.containerise( - name, + f"{asset}_{subset}", namespace, read_node, context, diff --git a/pype/plugins/harmony/load/load_palette.py b/pype/plugins/harmony/load/load_palette.py index 001758d5a8..ff85fdca5a 100644 --- a/pype/plugins/harmony/load/load_palette.py +++ b/pype/plugins/harmony/load/load_palette.py @@ -2,7 +2,6 @@ import os import shutil from avalon import api, harmony -from avalon.vendor import Qt class ImportPaletteLoader(api.Loader): @@ -41,14 +40,14 @@ class ImportPaletteLoader(api.Loader): harmony.save_scene() - # Dont allow instances with the same name. - message_box = Qt.QtWidgets.QMessageBox() - message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) msg = "Updated {}.".format(subset_name) msg += " You need to reload the scene to see the changes." - message_box.setText(msg) - message_box.exec_() + harmony.send( + { + "function": "PypeHarmony.message", + "args": msg + }) return name def remove(self, container): diff --git a/pype/plugins/harmony/load/load_template.py b/pype/plugins/harmony/load/load_template.py new file mode 100644 index 0000000000..59135c7931 --- /dev/null +++ b/pype/plugins/harmony/load/load_template.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +"""Load template.""" +import tempfile +import zipfile +import os +import shutil +import uuid + +from avalon import api, harmony +import pype.lib + + +class TemplateLoader(api.Loader): + """Load Harmony template as container. + + .. todo:: + + This must be implemented properly. + + """ + + families = ["template", "workfile"] + representations = ["*"] + label = "Load Template" + icon = "gift" + + def load(self, context, name=None, namespace=None, data=None): + """Plugin entry point. + + Args: + context (:class:`pyblish.api.Context`): Context. + name (str, optional): Container name. + namespace (str, optional): Container namespace. + data (dict, optional): Additional data passed into loader. + + """ + # Load template. + self_name = self.__class__.__name__ + temp_dir = tempfile.mkdtemp() + zip_file = api.get_representation_path(context["representation"]) + template_path = os.path.join(temp_dir, "temp.tpl") + with zipfile.ZipFile(zip_file, "r") as zip_ref: + zip_ref.extractall(template_path) + + group_id = "{}".format(uuid.uuid4()) + + container_group = harmony.send( + { + "function": f"PypeHarmony.Loaders.{self_name}.loadContainer", + "args": [template_path, + context["asset"]["name"], + context["subset"]["name"], + group_id] + } + )["result"] + + # Cleanup the temp directory + shutil.rmtree(temp_dir) + + # We must validate the group_node + return harmony.containerise( + name, + namespace, + container_group, + context, + self_name + ) + + def update(self, container, representation): + """Update loaded containers. + + Args: + container (dict): Container data. + representation (dict): Representation data. + + """ + node_name = container["name"] + node = harmony.find_node_by_name(node_name, "GROUP") + self_name = self.__class__.__name__ + + update_and_replace = False + if pype.lib.is_latest(representation): + self._set_green(node) + else: + self._set_red(node) + + update_and_replace = harmony.send( + { + "function": f"PypeHarmony.Loaders.{self_name}." + "askForColumnsUpdate", + "args": [] + } + )["result"] + + if update_and_replace: + # FIXME: This won't work, need to implement it. + harmony.send( + { + "function": f"PypeHarmony.Loaders.{self_name}." + "replaceNode", + "args": [] + } + ) + else: + self.load( + container["context"], container["name"], + None, container["data"]) + + harmony.imprint( + node, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + """Remove container. + + Args: + container (dict): container definition. + + """ + node = harmony.find_node_by_name(container["name"], "GROUP") + harmony.send( + {"function": "PypeHarmony.deleteNode", "args": [node]} + ) + + def switch(self, container, representation): + """Switch representation containers.""" + self.update(container, representation) + + def _set_green(self, node): + """Set node color to green `rgba(0, 255, 0, 255)`.""" + harmony.send( + { + "function": "PypeHarmony.setColor", + "args": [node, [0, 255, 0, 255]] + }) + + def _set_red(self, node): + """Set node color to red `rgba(255, 0, 0, 255)`.""" + harmony.send( + { + "function": "PypeHarmony.setColor", + "args": [node, [255, 0, 0, 255]] + }) diff --git a/pype/plugins/harmony/publish/collect_instances.py b/pype/plugins/harmony/publish/collect_instances.py index 8769ac80cc..aab89482ab 100644 --- a/pype/plugins/harmony/publish/collect_instances.py +++ b/pype/plugins/harmony/publish/collect_instances.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Collect instances in Harmony.""" import json import pyblish.api @@ -8,7 +10,7 @@ class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by nodes metadata. This collector takes into account assets that are associated with - a composite node and marked with a unique identifier; + a composite node and marked with a unique identifier. Identifier: id (str): "pyblish.avalon.instance" @@ -19,10 +21,19 @@ class CollectInstances(pyblish.api.ContextPlugin): hosts = ["harmony"] families_mapping = { "render": ["imagesequence", "review", "ftrack"], - "harmony.template": [] + "harmony.template": [], + "palette": ["palette", "ftrack"] } + pair_media = True + def process(self, context): + """Plugin entry point. + + Args: + context (:class:`pyblish.api.Context`): Context data. + + """ nodes = harmony.send( {"function": "node.subNodes", "args": ["Top"]} )["result"]