def workfiles_tool(self): if self._workfiles_tool is not None: return self._workfiles_tool from openpype.tools.workfiles.app import (Window, validate_host_requirements) # Host validation host = api.registered_host() validate_host_requirements(host) window = Window() window.refresh() window.setWindowFlags(window.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) # window.setStyleSheet(style.load_stylesheet()) context = { "asset": api.Session["AVALON_ASSET"], "silo": api.Session["AVALON_SILO"], "task": api.Session["AVALON_TASK"] } window.set_context(context) self._workfiles_tool = window return window
def check_inventory(): if not lib.any_outdated(): return host = api.registered_host() outdated_containers = [] for container in host.ls(): representation = container['representation'] representation_doc = io.find_one( { "_id": io.ObjectId(representation), "type": "representation" }, projection={"parent": True}) if representation_doc and not lib.is_latest(representation_doc): outdated_containers.append(container) # Warn about outdated containers. print("Starting new QApplication..") app = Qt.QtWidgets.QApplication(sys.argv) message_box = Qt.QtWidgets.QMessageBox() message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) msg = "There are outdated containers in the scene." message_box.setText(msg) message_box.exec_() # Garbage collect QApplication. del app
def get_all_asset_nodes(): """Get all assets from the scene, container based""" host = api.registered_host() nodes = list() for container in host.ls(): # We only interested in surface assets ! # (TODO): This black list should be somewhere else if container["loader"] in ("LookLoader", "CameraLoader", "LightSetLoader"): continue # Gather all information container_name = container["objectName"] subset = container["name"] namespace = container["namespace"] for node in cmds.ls( cmds.sets(container_name, query=True, nodesOnly=True)): asset_id = get_asset_id(node) if asset_id is None: continue nodes.append({ "node": node, "assetId": asset_id, "subset": subset, "namespace": namespace, }) return nodes
def remove_unused_looks(): """Removes all loaded looks for which none of the shaders are used. This will cleanup all loaded "LookLoader" containers that are unused in the current scene. """ host = api.registered_host() unused = list() for container in host.ls(): if container['loader'] == "LookLoader": members = cmds.sets(container['objectName'], query=True) look_sets = cmds.ls(members, type="objectSet") for look_set in look_sets: # If the set is used than we consider this look *in use* if cmds.sets(look_set, query=True): break else: unused.append(container) for container in unused: log.info("Removing unused look container: %s", container['objectName']) api.remove(container) log.info("Finished removing unused looks. (see log for details)")
def assign_look_by_version(nodes, version_id): """Assign nodes a specific published look version by id. This assumes the nodes correspond with the asset. Args: nodes(list): nodes to assign look to version_id (bson.ObjectId): database id of the version Returns: None """ # Get representations of shader file and relationships look_representation = io.find_one({ "type": "representation", "parent": version_id, "name": "ma" }) json_representation = io.find_one({ "type": "representation", "parent": version_id, "name": "json" }) # See if representation is already loaded, if so reuse it. host = api.registered_host() representation_id = str(look_representation['_id']) for container in host.ls(): if (container['loader'] == "LookLoader" and container['representation'] == representation_id): log.info("Reusing loaded look ..") container_node = container['objectName'] break else: log.info("Using look for the first time ..") # Load file loaders = api.loaders_from_representation(api.discover(api.Loader), representation_id) Loader = next((i for i in loaders if i.__name__ == "LookLoader"), None) if Loader is None: raise RuntimeError("Could not find LookLoader, this is a bug") # Reference the look file with maya.maintained_selection(): container_node = pipeline.load(Loader, look_representation) # Get container members shader_nodes = cmds.sets(container_node, query=True) # Load relationships shader_relation = api.get_representation_path(json_representation) with open(shader_relation, "r") as f: relationships = json.load(f) # Assign relationships apply_shaders(relationships, shader_nodes, nodes)
def from_workfile(self, additional_jobs=None): """Generate jobs from workfile (DCC App agnostic) Args: additional_jobs (list, optional): A list of callbacks """ # Add workfile session = api.Session host = api.registered_host() workfile = host.current_file() if workfile is None: # Must be saved since we are parsing workfile here raise Exception("Could not obtain workfile path.") # Compute remote work dir project = io.find_one({"type": "project"}) template = project["config"]["template"]["work"] remote_path = template.format( **{ "root": self.remote_root, "project": session["AVALON_PROJECT"], "silo": session["AVALON_SILO"], "asset": session["AVALON_ASSET"], "task": session["AVALON_TASK"], "app": session["AVALON_APP"], "user": self.remote_user, }) workfile_name = os.path.basename(workfile) local_user = session.get("AVALON_USER", getpass.getuser()) # Prevent workfile overwrite when the remote username is not the # same as local username. This happens when multiple local users # using same remote account to access remote machine. same_user = local_user == self.remote_user if not same_user and "{user}" in template: # Prefix local username into file name workfile_name = local_user + "_" + workfile_name remote_path += "/scenes/" # AVALON_SCENEDIR remote_path += workfile_name workfile = os.path.normpath(workfile) remote_path = os.path.normpath(remote_path) # Add workfile job self.add_job(files=[(workfile, remote_path)], type="Workfile", description="%s - %s" % (session["AVALON_ASSET"], os.path.basename(workfile))) # Additional jobs for job in additional_jobs or []: job()
def parse_containers(self): """ NOTE: This is the additional job for workfile """ def texture_lookup(version_id): representations = set() dependent = "data.dependents.%s" % version_id filter = { "type": "version", "data.families": "reveries.texture", dependent: { "$exists": True }, } version = io.find_one(filter) if version is not None: representation = io.find_one({"parent": version["_id"]}) representations.add(str(representation["_id"])) # Patching textures for pre_version in io.find( { "parent": version["parent"], "name": { "$lt": version["name"] } }, sort=[("name", -1)]): pre_repr = io.find_one({"parent": pre_version["_id"]}) if "fileInventory" in pre_repr["data"]: representations.add(str(pre_repr["_id"])) else: break return representations # Start representations = set() versions = set() for container in api.registered_host().ls(): representations.add(container["representation"]) versions.add(container["versionId"]) for id in versions: representations.update(texture_lookup(id)) for id in representations: self.from_representation(id)
def patched_discover(superclass): """ Monkey patched version of :func:`avalon.api.discover()`. It allows us to load presets on plugins being discovered. """ # run original discover and get plugins plugins = _original_discover(superclass) # determine host application to use for finding presets if avalon.registered_host() is None: return plugins host = avalon.registered_host().__name__.split(".")[-1] # map plugin superclass to preset json. Currenly suppoted is load and # create (avalon.api.Loader and avalon.api.Creator) plugin_type = "undefined" if superclass.__name__.split(".")[-1] == "Loader": plugin_type = "load" elif superclass.__name__.split(".")[-1] == "Creator": plugin_type = "create" print(">>> trying to find presets for {}:{} ...".format(host, plugin_type)) try: config_data = config.get_presets()['plugins'][host][plugin_type] except KeyError: print("*** no presets found.") else: for plugin in plugins: if plugin.__name__ in config_data: print(">>> We have preset for {}".format(plugin.__name__)) for option, value in config_data[plugin.__name__].items(): if option == "enabled" and value is False: setattr(plugin, "active", False) print(" - is disabled by preset") else: setattr(plugin, option, value) print(" - setting `{}`: `{}`".format(option, value)) return plugins
def export(self, out=None): """Write out JSON format job package file Args: out (str, optional): Output file path """ if out is None: host = api.registered_host() workfile = host.current_file() or "temp" out = os.path.abspath(workfile + ".sftp.job") with open(out, "w") as file: json.dump(self.jobs, file, indent=4) return out
def get_containers(): """Collect all containers in the scene and collect all their nodes Returns: generator object """ host = api.registered_host() for container in host.ls(): nodes = cmds.sets(container["objectName"], query=True, nodesOnly=True) nodes = cmds.ls(nodes, long=True) # Update container container.update({"nodes": create_id_hash(nodes)}) yield container
def get_all_asset_nodes(): """Get all assets from the scene, container based""" host = api.registered_host() nodes = list() for container in host.ls(): # We only interested in surface assets ! # (TODO): This black list should be somewhere else if container["loader"] in ("LookLoader", "CameraLoader", "ArnoldAssLoader", "SetDressLoader", "LightSetLoader"): continue # Gather all information subset = container["name"] namespace = container["namespace"] # (NOTE) Previously, nodes was collecting from container objectSet, # but container may lost it's member nodes during production. # So change to get asset nodes from hierarchy to ensure user # always gets all loaded subsets. group = container.get("subsetGroup") if not group: continue for node in cmds.listRelatives(group, allDescendents=True, path=True, type="transform"): asset_id = get_asset_id(node) if asset_id is None: continue nodes.append({ "node": node, "assetId": asset_id, "subset": subset, "namespace": namespace, }) return nodes
def get_containers(nodes): """Get containers for the nodes Args: nodes (list): collect of strings, e.g: selected nodes Return: dict """ host = api.registered_host() results = {} nodes = set(nodes) for container in host.ls(): container_object = container['objectName'] members = set(cmds.sets(container_object, query=True) or []) if nodes.intersection(members): results[container_object] = list(members) return results
def get_all_asset_nodes(): """Get all assets from the scene, container based Returns: list: list of dictionaries """ host = api.registered_host() nodes = [] for container in host.ls(): # We are not interested in looks but assets! if container["loader"] == "LookLoader": continue # Gather all information container_name = container["objectName"] nodes += cmds.sets(container_name, query=True, nodesOnly=True) or [] return nodes
def get_all_asset_nodes(): """Get all assets from the scene, container based Returns: list: list of dictionaries """ host = api.registered_host() nodes = [] for container in host.ls(): # We only interested in surface assets ! # (TODO): This black list should be somewhere else if container["loader"] in ("LookLoader", "CameraLoader", "LightSetLoader"): continue # Gather all information container_name = container["objectName"] nodes += cmds.sets(container_name, query=True, nodesOnly=True) or [] return nodes
def reset(self): self.blockSignals(True) self.clear() self.blockSignals(False) host = api.registered_host() if host is None: return self.beginResetModel() self.appendRow(self.placeholder_item) for container in host.ls(): if lib.is_supported_loader(container["loader"]): item = QtGui.QStandardItem(container["namespace"][1:]) item.setData(container, QtCore.Qt.UserRole) self.appendRow(item) self.endResetModel()
def show(root=None, debug=False, parent=None, use_context=True, save=True): """Show Work Files GUI""" # todo: remove `root` argument to show() try: module.window.close() del (module.window) except (AttributeError, RuntimeError): pass host = api.registered_host() validate_host_requirements(host) if debug: api.Session["AVALON_ASSET"] = "Mock" api.Session["AVALON_TASK"] = "Testing" with tools_lib.application(): window = Window(parent=parent) window.refresh() if use_context: context = { "asset": api.Session["AVALON_ASSET"], "silo": api.Session["AVALON_SILO"], "task": api.Session["AVALON_TASK"] } window.set_context(context) window.files_widget.btn_save.setEnabled(save) window.show() window.setStyleSheet(style.load_stylesheet()) module.window = window # Pull window to the front. module.window.raise_() module.window.activateWindow()
def parse_stray_textures(self): """Find file nodes which pointing files that were not in published space If there are any texture files that has not been published... NOTE: This is the additional job for workfile """ session = api.Session host = api.registered_host() workfile = host.current_file() project = session["AVALON_PROJECT"] jobs = list() for file_node in find_stray_textures(): file_path = cmds.getAttr(file_node + ".fileTextureName", expandEnvironmentVariables=True) if project in file_path: head, tail = file_path.split(project, 1) # Replace root remote_path = self.remote_root + os.sep + project + tail file_path = os.path.normpath(file_path) remote_path = os.path.normpath(remote_path) jobs.append((file_path, remote_path)) if not jobs: return self.add_job(files=jobs, type="Stray Textures", description="%s - %s" % (session["AVALON_ASSET"], os.path.basename(workfile)))
def get_all_assets(): """Get all assets from the scene Returns: list """ host = api.registered_host() items = [] for container in host.ls(): # We are not interested in looks but assets! if container["loader"] == "LookLoader": continue # Gather all information container_name = container["objectName"] content = cmds.sets(container_name, query=True) item = create_item_from_container(container_name, content) if not item: continue items.append(item) return items
def __init__(self, parent=None): super(FilesWidget, self).__init__(parent=parent) # Setup self._asset = None self._task = None # Pype's anatomy object for current project self.anatomy = Anatomy(io.Session["AVALON_PROJECT"]) # Template key used to get work template from anatomy templates # TODO change template key based on task self.template_key = "work" # This is not root but workfile directory self.root = None self.host = api.registered_host() # Whether to automatically select the latest modified # file on a refresh of the files model. self.auto_select_latest_modified = True # Avoid crash in Blender and store the message box # (setting parent doesn't work as it hides the message box) self._messagebox = None files_view = FilesView(self) # Create the Files model extensions = set(self.host.file_extensions()) files_model = FilesModel(file_extensions=extensions) # Create proxy model for files to be able sort and filter proxy_model = QtCore.QSortFilterProxyModel() proxy_model.setSourceModel(files_model) proxy_model.setDynamicSortFilter(True) proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) # Set up the file list tree view files_view.setModel(proxy_model) files_view.setSortingEnabled(True) files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # Date modified delegate time_delegate = PrettyTimeDelegate() files_view.setItemDelegateForColumn(1, time_delegate) files_view.setIndentation(3) # smaller indentation # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. files_view.setColumnWidth(0, 330) # Filtering input filter_input = QtWidgets.QLineEdit(self) filter_input.textChanged.connect(proxy_model.setFilterFixedString) filter_input.setPlaceholderText("Filter files..") # Home Page # Build buttons widget for files widget btns_widget = QtWidgets.QWidget(self) btn_save = QtWidgets.QPushButton("Save As", btns_widget) btn_browse = QtWidgets.QPushButton("Browse", btns_widget) btn_open = QtWidgets.QPushButton("Open", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addWidget(btn_open) btns_layout.addWidget(btn_browse) btns_layout.addWidget(btn_save) # Build files widgets for home page main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(filter_input) main_layout.addWidget(files_view) main_layout.addWidget(btns_widget) # Register signal callbacks files_view.doubleClickedLeft.connect(self.on_open_pressed) files_view.customContextMenuRequested.connect(self.on_context_menu) files_view.selectionModel().selectionChanged.connect( self.on_file_select) btn_open.pressed.connect(self.on_open_pressed) btn_browse.pressed.connect(self.on_browse_pressed) btn_save.pressed.connect(self.on_save_as_pressed) # Store attributes self.time_delegate = time_delegate self.filter_input = filter_input self.files_view = files_view self.files_model = files_model self.btns_widget = btns_widget self.btn_open = btn_open self.btn_browse = btn_browse self.btn_save = btn_save
def __init__(self, parent, root, anatomy, template_key, session=None): super(NameWindow, self).__init__(parent=parent) self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.result = None self.host = api.registered_host() self.root = root self.work_file = None if not session: # Fallback to active session session = api.Session # Set work file data for template formatting asset_name = session["AVALON_ASSET"] project_doc = io.find_one({"type": "project"}) self.data = { "project": { "name": project_doc["name"], "code": project_doc["data"].get("code") }, "asset": asset_name, "task": session["AVALON_TASK"], "version": 1, "user": getpass.getuser(), "comment": "", "ext": None } # Store project anatomy self.anatomy = anatomy self.template = anatomy.templates[template_key]["file"] self.template_key = template_key # Btns widget btns_widget = QtWidgets.QWidget(self) btn_ok = QtWidgets.QPushButton("Ok", btns_widget) btn_cancel = QtWidgets.QPushButton("Cancel", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.addWidget(btn_ok) btns_layout.addWidget(btn_cancel) # Inputs widget inputs_widget = QtWidgets.QWidget(self) # Version widget version_widget = QtWidgets.QWidget(inputs_widget) # Version number input version_input = QtWidgets.QSpinBox(version_widget) version_input.setMinimum(1) version_input.setMaximum(9999) # Last version checkbox last_version_check = QtWidgets.QCheckBox("Next Available Version", version_widget) last_version_check.setChecked(True) version_layout = QtWidgets.QHBoxLayout(version_widget) version_layout.setContentsMargins(0, 0, 0, 0) version_layout.addWidget(version_input) version_layout.addWidget(last_version_check) # Preview widget preview_label = QtWidgets.QLabel("Preview filename", inputs_widget) # Subversion input subversion_input = QtWidgets.QLineEdit(inputs_widget) subversion_input.setPlaceholderText("Will be part of filename.") # Extensions combobox ext_combo = QtWidgets.QComboBox(inputs_widget) ext_combo.addItems(self.host.file_extensions()) # Build inputs inputs_layout = QtWidgets.QFormLayout(inputs_widget) # Add version only if template contain version key # - since the version can be padded with "{version:0>4}" we only search # for "{version". if "{version" in self.template: inputs_layout.addRow("Version:", version_widget) # Add subversion only if template containt `{comment}` if "{comment}" in self.template: inputs_layout.addRow("Subversion:", subversion_input) inputs_layout.addRow("Extension:", ext_combo) inputs_layout.addRow("Preview:", preview_label) # Build layout main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(inputs_widget) main_layout.addWidget(btns_widget) # Singal callback registration version_input.valueChanged.connect(self.on_version_spinbox_changed) last_version_check.stateChanged.connect( self.on_version_checkbox_changed) subversion_input.textChanged.connect(self.on_comment_changed) ext_combo.currentIndexChanged.connect(self.on_extension_changed) btn_ok.pressed.connect(self.on_ok_pressed) btn_cancel.pressed.connect(self.on_cancel_pressed) # Allow "Enter" key to accept the save. btn_ok.setDefault(True) # Force default focus to comment, some hosts didn't automatically # apply focus to this line edit (e.g. Houdini) subversion_input.setFocus() # Store widgets self.btn_ok = btn_ok self.version_widget = version_widget self.version_input = version_input self.last_version_check = last_version_check self.preview_label = preview_label self.subversion_input = subversion_input self.ext_combo = ext_combo self.refresh()
def switch(asset_name, filepath=None, new=True): """Switch the current containers of the file to the other asset (shot) Args: filepath (str): file path of the comp file asset_name (str): name of the asset (shot) new (bool): Save updated comp under a different name Returns: comp path (str): new filepath of the updated comp """ # If filepath provided, ensure it is valid absolute path if filepath is not None: if not os.path.isabs(filepath): filepath = os.path.abspath(filepath) assert os.path.exists(filepath), "%s must exist " % filepath # Assert asset name exists # It is better to do this here then to wait till switch_shot does it asset = io.find_one({"type": "asset", "name": asset_name}) assert asset, "Could not find '%s' in the database" % asset_name # Get current project self._project = io.find_one({ "type": "project", "name": api.Session["AVALON_PROJECT"] }) # Go to comp if not filepath: current_comp = avalon.fusion.get_current_comp() assert current_comp is not None, "Could not find current comp" else: fusion = _get_fusion_instance() current_comp = fusion.LoadComp(filepath, quiet=True) assert current_comp is not None, "Fusion could not load '%s'" % filepath host = api.registered_host() containers = list(host.ls()) assert containers, "Nothing to update" representations = [] for container in containers: try: representation = pype.switch_item(container, asset_name=asset_name) representations.append(representation) except Exception as e: current_comp.Print("Error in switching! %s\n" % e.message) message = "Switched %i Loaders of the %i\n" % (len(representations), len(containers)) current_comp.Print(message) # Build the session to switch to switch_to_session = api.Session.copy() switch_to_session["AVALON_ASSET"] = asset['name'] if new: comp_path = _format_filepath(switch_to_session) # Update savers output based on new session _update_savers(current_comp, switch_to_session) else: comp_path = pype.version_up(filepath) current_comp.Print(comp_path) current_comp.Print("\nUpdating frame range") update_frame_range(current_comp, representations) current_comp.Save(comp_path) return comp_path
def refresh(self): model_containers = list() host = api.registered_host() for container in host.ls(): if container["loader"] == "ModelLoader": model_containers.append(container) self.clear() self.beginResetModel() for container in model_containers: subset_id = io.ObjectId(container["subsetId"]) version_id = io.ObjectId(container["versionId"]) version = io.find_one({"_id": version_id}) latest = io.find_one({ "type": "version", "parent": subset_id }, sort=[("name", -1)], projection={"name": True}) latest_repr = io.find_one({ "type": "representation", "parent": latest["_id"], "name": "mayaBinary" }) # Is latest version loaded ? is_latest = latest["name"] == version["name"] versions = io.find({ "type": "version", "parent": subset_id }, sort=[("name", -1)]) for version in versions: repr = io.find_one({ "type": "representation", "parent": version["_id"], "name": "mayaBinary" }) protected = repr["data"].get("modelProtected") if protected is not None: # Get protected list from previous version if not found break protected = protected or set() namespace = container["namespace"] subset_group = container["subsetGroup"] subset_item = models.Item() subset_item.update({ "subsetId": subset_id, "representation": latest_repr, "namespace": namespace, "node": subset_group, "name": subset_group.rsplit("|", 1)[-1][len(namespace):], "isLatest": is_latest, }) members = cmds.sets(container["objectName"], query=True) for node in cmds.ls(members, type="transform", referencedNodes=True, long=True): meshes = cmds.listRelatives(node, shapes=True, noIntermediate=True, type="mesh") if not meshes: continue id = utils.get_id(node) is_locked = id in protected node_item = models.Item() node_item.update({ "node": node, "name": node.rsplit("|", 1)[-1][len(namespace):], "avalonId": id, "isLocked": is_locked, "isLatest": is_latest, "setLocked": None, }) subset_item.add_child(node_item) self.add_child(subset_item) self.endResetModel()