diff --git a/openpype/plugins/publish/collect_scene_loaded_versions.py b/openpype/plugins/publish/collect_scene_loaded_versions.py new file mode 100644 index 0000000000..c26b322df2 --- /dev/null +++ b/openpype/plugins/publish/collect_scene_loaded_versions.py @@ -0,0 +1,55 @@ + +import pyblish.api +from avalon import api, io + + +class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): + + order = pyblish.api.CollectorOrder + 0.0001 + label = "Collect Versions Loaded in Scene" + hosts = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint" + ] + + def process(self, context): + host = api.registered_host() + if host is None: + self.log.warn("No registered host.") + return + + if not hasattr(host, "ls"): + host_name = host.__name__ + self.log.warn("Host %r doesn't have ls() implemented." % host_name) + return + + loaded_versions = [] + _containers = list(host.ls()) + _repr_ids = [io.ObjectId(c["representation"]) for c in _containers] + version_by_repr = { + str(doc["_id"]): doc["parent"] for doc in + io.find({"_id": {"$in": _repr_ids}}, projection={"parent": 1}) + } + + for con in _containers: + # NOTE: + # may have more then one representation that are same version + version = { + "objectName": con["objectName"], # container node name + "subsetName": con["name"], + "representation": io.ObjectId(con["representation"]), + "version": version_by_repr[con["representation"]], # _id + } + loaded_versions.append(version) + + context.data["loadedVersions"] = loaded_versions diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index ca12f2900c..8ed6e25e66 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -10,7 +10,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): """ order = pyblish.api.CollectorOrder - label = 'Collect Version' + label = 'Collect Scene Version' hosts = [ "aftereffects", "blender", diff --git a/openpype/plugins/publish/integrate_inputlinks.py b/openpype/plugins/publish/integrate_inputlinks.py new file mode 100644 index 0000000000..e8a8b2296c --- /dev/null +++ b/openpype/plugins/publish/integrate_inputlinks.py @@ -0,0 +1,130 @@ + +from collections import OrderedDict +from avalon import io +import pyblish.api + + +class IntegrateInputLinks(pyblish.api.ContextPlugin): + """Connecting version level dependency links""" + + order = pyblish.api.IntegratorOrder + 0.2 + label = "Connect Dependency InputLinks" + + def process(self, context): + """Connect dependency links for all instances, globally + + Code steps: + * filter out instances that has "versionEntity" entry in data + * find workfile instance within context + * if workfile found: + - link all `loadedVersions` as input of the workfile + - link workfile as input of all publishing instances + * else: + - show "no workfile" warning + * link instances' inputs if it's data has "inputVersions" entry + * Write into database + + inputVersions: + The "inputVersions" in instance.data should be a list of + version document's Id (str or ObjectId), which are the + dependencies of the publishing instance that should be + extracted from working scene by the DCC specific publish + plugin. + + """ + workfile = None + publishing = [] + + for instance in context: + if not instance.data.get("publish", True): + # Skip inactive instances + continue + + version_doc = instance.data.get("versionEntity") + if not version_doc: + self.log.debug("Instance %s doesn't have version." % instance) + continue + + version_data = version_doc.get("data", {}) + families = version_data.get("families", []) + + if "workfile" in families: + workfile = instance + else: + publishing.append(instance) + + if workfile is None: + self.log.warn("No workfile in this publish session.") + else: + workfile_version_doc = workfile.data["versionEntity"] + # link all loaded versions in scene into workfile + for version in context.data.get("loadedVersions", []): + self.add_link( + link_type="reference", + input_id=version["version"], + version_doc=workfile_version_doc, + ) + # link workfile to all publishing versions + for instance in publishing: + self.add_link( + link_type="generative", + input_id=workfile_version_doc["_id"], + version_doc=instance.data["versionEntity"], + ) + + # link versions as dependencies to the instance + for instance in publishing: + for input_version in instance.data.get("inputVersions") or []: + self.add_link( + link_type="generative", + input_id=input_version, + version_doc=instance.data["versionEntity"], + ) + + publishing.append(workfile) + self.write_links_to_database(publishing) + + def add_link(self, link_type, input_id, version_doc): + """Add dependency link data into version document + + Args: + link_type (str): Type of link, one of 'reference' or 'generative' + input_id (str or ObjectId): Document Id of input version + version_doc (dict): The version document that takes the input + + Returns: + None + + """ + # NOTE: + # using OrderedDict() here is just for ensuring field order between + # python versions, if we ever need to use mongodb operation '$addToSet' + # to update and avoid duplicating elements in 'inputLinks' array in the + # future. + link = OrderedDict() + link["type"] = link_type + link["input"] = io.ObjectId(input_id) + link["linkedBy"] = "publish" + + if "inputLinks" not in version_doc["data"]: + version_doc["data"]["inputLinks"] = [] + version_doc["data"]["inputLinks"].append(link) + + def write_links_to_database(self, instances): + """Iter instances in context to update database + + If `versionEntity.data.inputLinks` not None in `instance.data`, doc + in database will be updated. + + """ + for instance in instances: + version_doc = instance.data.get("versionEntity") + if version_doc is None: + continue + + input_links = version_doc["data"].get("inputLinks") + if input_links is None: + continue + + io.update_one({"_id": version_doc["_id"]}, + {"$set": {"data.inputLinks": input_links}}) diff --git a/openpype/tools/assetlinks/__init__.py b/openpype/tools/assetlinks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/assetlinks/widgets.py b/openpype/tools/assetlinks/widgets.py new file mode 100644 index 0000000000..9a136462b0 --- /dev/null +++ b/openpype/tools/assetlinks/widgets.py @@ -0,0 +1,85 @@ + +from Qt import QtWidgets + + +class SimpleLinkView(QtWidgets.QWidget): + + def __init__(self, dbcon, parent=None): + super(SimpleLinkView, self).__init__(parent=parent) + self.dbcon = dbcon + + # TODO: display selected target + + in_text = QtWidgets.QLabel("Inputs") + in_view = QtWidgets.QListWidget(parent=self) + out_text = QtWidgets.QLabel("Outputs") + out_view = QtWidgets.QListWidget(parent=self) + + layout = QtWidgets.QGridLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(in_text, 0, 0) + layout.addWidget(in_view, 1, 0) + layout.addWidget(out_text, 0, 1) + layout.addWidget(out_view, 1, 1) + + self._in_view = in_view + self._out_view = out_view + + def clear(self): + self._in_view.clear() + self._out_view.clear() + + def set_version(self, version_doc): + self.clear() + if not version_doc or not self.isVisible(): + return + + # inputs + # + for link in version_doc["data"].get("inputLinks", []): + version = self.dbcon.find_one( + {"_id": link["input"], "type": "version"}, + projection={"name": 1, "parent": 1} + ) + if not version: + continue + subset = self.dbcon.find_one( + {"_id": version["parent"], "type": "subset"}, + projection={"name": 1, "parent": 1} + ) + if not subset: + continue + asset = self.dbcon.find_one( + {"_id": subset["parent"], "type": "asset"}, + projection={"name": 1} + ) + + self._in_view.addItem("{asset} {subset} v{version:0>3}".format( + asset=asset["name"], + subset=subset["name"], + version=version["name"], + )) + + # outputs + # + outputs = self.dbcon.find( + {"type": "version", "data.inputLinks.input": version_doc["_id"]}, + projection={"name": 1, "parent": 1} + ) + for version in outputs or []: + subset = self.dbcon.find_one( + {"_id": version["parent"], "type": "subset"}, + projection={"name": 1, "parent": 1} + ) + if not subset: + continue + asset = self.dbcon.find_one( + {"_id": subset["parent"], "type": "asset"}, + projection={"name": 1} + ) + + self._out_view.addItem("{asset} {subset} v{version:0>3}".format( + asset=asset["name"], + subset=subset["name"], + version=version["name"], + )) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index f14f58dfb4..c7138d8f72 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -21,6 +21,7 @@ from openpype.tools.utils.views import ( TreeViewSpinner, DeselectableTreeView ) +from openpype.tools.assetlinks.widgets import SimpleLinkView from .model import ( SubsetsModel, @@ -845,19 +846,25 @@ class VersionWidget(QtWidgets.QWidget): def __init__(self, dbcon, parent=None): super(VersionWidget, self).__init__(parent=parent) - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - label = QtWidgets.QLabel("Version", self) data = VersionTextEdit(dbcon, self) data.setReadOnly(True) - layout.addWidget(label) - layout.addWidget(data) + depend_widget = SimpleLinkView(dbcon, self) + + tab = QtWidgets.QTabWidget() + tab.addTab(data, "Version Info") + tab.addTab(depend_widget, "Dependency") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(tab) self.data = data + self.depend_widget = depend_widget def set_version(self, version_doc): self.data.set_version(version_doc) + self.depend_widget.set_version(version_doc) class FamilyModel(QtGui.QStandardItemModel):