From d93d03abf0e6238f0a17b1ade96e0d43fdf63082 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 May 2024 13:47:54 +0200 Subject: [PATCH 01/65] update resolve documentation --- ...ld6.txt => RESOLVE_API_v19.0B-build20.txt} | 170 +++++++++++++++--- 1 file changed, 150 insertions(+), 20 deletions(-) rename client/ayon_core/hosts/resolve/{RESOLVE_API_v18.5.1-build6.txt => RESOLVE_API_v19.0B-build20.txt} (86%) diff --git a/client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt b/client/ayon_core/hosts/resolve/RESOLVE_API_v19.0B-build20.txt similarity index 86% rename from client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt rename to client/ayon_core/hosts/resolve/RESOLVE_API_v19.0B-build20.txt index 7d1d6edf61..a2f3fa6f73 100644 --- a/client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt +++ b/client/ayon_core/hosts/resolve/RESOLVE_API_v19.0B-build20.txt @@ -1,26 +1,23 @@ -Updated as of 26 May 2023 +Last Updated: 1 April 2024 ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. From v16.2.0 onwards, the nodeIndex parameters accepted by SetLUT() and SetCDL() are 1-based instead of 0-based, i.e. 1 <= nodeIndex <= total number of nodes. - Overview -------- -As with Blackmagic Design Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page, +As with Blackmagic Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page, or via command line. This permission can be changed in Resolve Preferences, to be only from Console, or to be invoked from the local network. Please be aware of the security implications when allowing scripting access from outside of the Resolve application. - Prerequisites ------------- DaVinci Resolve scripting requires one of the following to be installed (for all users): Lua 5.1 - Python 2.7 64-bit Python >= 3.6 64-bit - + Python 2.7 64-bit Using a script -------------- @@ -64,6 +61,7 @@ The interactive Console window allows for an easy way to execute simple scriptin and Lua and evaluates and executes them immediately. For more information on how to use the Console, please refer to the DaVinci Resolve User Manual. This example Python script creates a simple project: + #!/usr/bin/env python import DaVinciResolveScript as dvr_script resolve = dvr_script.scriptapp("Resolve") @@ -80,9 +78,8 @@ Running DaVinci Resolve in headless mode DaVinci Resolve can be launched in a headless mode without the user interface using the -nogui command line option. When DaVinci Resolve is launched using this option, the user interface is disabled. However, the various scripting APIs will continue to work as expected. - -Basic Resolve API ------------------ +DaVinci Resolve API +------------------- Some commonly used API functions are described below (*). As with the resolve object, each object is inspectable for properties and functions. Resolve @@ -101,6 +98,12 @@ Resolve SaveLayoutPreset(presetName) --> Bool # Saves current UI layout as a preset named 'presetName'. ImportLayoutPreset(presetFilePath, presetName) --> Bool # Imports preset from path 'presetFilePath'. The optional argument 'presetName' specifies how the preset shall be named. If not specified, the preset is named based on the filename. Quit() --> None # Quits the Resolve App. + ImportRenderPreset(presetPath) --> Bool # Import a preset from presetPath (string) and set it as current preset for rendering. + ExportRenderPreset(presetName, exportPath) --> Bool # Export a preset to a given path (string) if presetName(string) exists. + ImportBurnInPreset(presetPath) --> Bool # Import a data burn in preset from a given presetPath (string) + ExportBurnInPreset(presetName, exportPath) --> Bool # Export a data burn in preset to a given path (string) if presetName (string) exists. + GetKeyframeMode() --> keyframeMode # Returns the currently set keyframe mode (int). Refer to section 'Keyframe Mode information' below for details. + SetKeyframeMode(keyframeMode) --> Bool # Returns True when 'keyframeMode'(enum) is successfully set. Refer to section 'Keyframe Mode information' below for details. ProjectManager ArchiveProject(projectName, @@ -131,6 +134,14 @@ ProjectManager # 'DbType': 'Disk' or 'PostgreSQL' (string) # 'DbName': database name (string) # 'IpAddress': IP address of the PostgreSQL server (string, optional key - defaults to '127.0.0.1') + CreateCloudProject({cloudSettings}) --> Project # Creates and returns a cloud project. + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. + ImportCloudProject(filePath, {cloudSettings}) --> Bool # Returns True if import cloud project is successful; False otherwise + # 'filePath': String; filePath of file to import + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. + RestoreCloudProject(folderPath, {cloudSettings}) --> Bool # Returns True if restore cloud project is successful; False otherwise + # 'folderPath': String; path of folder to restore + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. Project GetMediaPool() --> MediaPool # Returns the Media Pool object. @@ -175,6 +186,9 @@ Project startOffsetInSamples, durationInSamples) LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for project when supplied presetName (string). Returns true if successful. ExportCurrentFrameAsStill(filePath) --> Bool # Exports current frame as still to supplied filePath. filePath must end in valid export file format. Returns True if succssful, False otherwise. + GetColorGroupsList() --> [ColorGroups...] # Returns a list of all group objects in the timeline. + AddColorGroup(groupName) --> ColorGroup # Creates a new ColorGroup. groupName must be a unique string. + DeleteColorGroup(colorGroup) --> Bool # Deletes the given color group and sets clips to ungrouped. MediaStorage GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage. @@ -198,7 +212,7 @@ MediaPool CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [clips]) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), "recordFrame" (int). - ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys: + ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL/OTIO) and optional importOptions dict, with support for the keys: # "timelineName": string, specifies the name of the timeline to be created. Not valid for DRT import # "importSourceClips": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import # "sourceClipsPath": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if "importSourceClips" is True @@ -225,6 +239,8 @@ MediaPool ExportMetadata(fileName, [clips]) --> Bool # Exports metadata of specified clips to 'fileName' in CSV format. # If no clips are specified, all clips from media pool will be used. GetUniqueId() --> string # Returns a unique ID for the media pool + CreateStereoClip(LeftMediaPoolItem, + RightMediaPoolItem) --> MediaPoolItem # Takes in two existing media pool items and creates a new 3D stereoscopic media pool entry replacing the input media in the media pool. Folder GetClipList() --> [clips...] # Returns a list of clips (items) within the folder. @@ -233,6 +249,8 @@ Folder GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise GetUniqueId() --> string # Returns a unique ID for the media pool folder Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise + TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise + ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise. MediaPoolItem GetName() --> string # Returns the clip name. @@ -340,8 +358,12 @@ Timeline GrabStill() --> galleryStill # Grabs still from the current video clip. Returns a GalleryStill object. GrabAllStills(stillFrameSource) --> [galleryStill] # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects. GetUniqueId() --> string # Returns a unique ID for the timeline - CreateSubtitlesFromAudio() --> Bool # Creates subtitles from audio for the timeline. Returns True on success, False otherwise. + CreateSubtitlesFromAudio({autoCaptionSettings}) --> Bool # Creates subtitles from audio for the timeline. + # Takes in optional dictionary {autoCaptionSettings}. Check 'Auto Caption Settings' subsection below for more information. + # Returns True on success, False otherwise. DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. + ConvertTimelineToStereo() --> Bool # Converts timeline to stereo. Returns True if successful; False otherwise. + GetNodeGraph() --> Graph # Returns the timeline's node graph object. TimelineItem GetName() --> string # Returns the item name. @@ -390,12 +412,7 @@ TimelineItem GetStereoConvergenceValues() --> {keyframes...} # Returns a dict (offset -> value) of keyframe offsets and respective convergence values. GetStereoLeftFloatingWindowParams() --> {keyframes...} # For the LEFT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetStereoRightFloatingWindowParams() --> {keyframes...} # For the RIGHT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. - GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item ApplyArriCdlLut() --> Bool # Applies ARRI CDL and LUT. Returns True if successful, False otherwise. - SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes. - # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). - # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). - GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes. SetCDL([CDL map]) --> Bool # Keys of map are: "NodeIndex", "Slope", "Offset", "Power", "Saturation", where 1 <= NodeIndex <= total number of nodes. # Example python code - SetCDL({"NodeIndex" : "1", "Slope" : "0.5 0.4 0.2", "Offset" : "0.4 0.3 0.2", "Power" : "0.6 0.7 0.8", "Saturation" : "0.65"}) AddTake(mediaPoolItem, startFrame, endFrame) --> Bool # Adds mediaPoolItem as a new take. Initializes a take selector for the timeline item if needed. By default, the full clip extents is added. startFrame (int) and endFrame (int) are optional arguments used to specify the extents. @@ -411,11 +428,17 @@ TimelineItem UpdateSidecar() --> Bool # Updates sidecar file for BRAW clips or RMD file for R3D clips. GetUniqueId() --> string # Returns a unique ID for the timeline item LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for clip when supplied presetName (string). Returns true if successful. - GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. CreateMagicMask(mode) --> Bool # Returns True if magic mask was created successfully, False otherwise. mode can "F" (forward), "B" (backward), or "BI" (bidirection) RegenerateMagicMask() --> Bool # Returns True if magic mask was regenerated successfully, False otherwise. Stabilize() --> Bool # Returns True if stabilization was successful, False otherwise SmartReframe() --> Bool # Performs Smart Reframe. Returns True if successful, False otherwise. + GetNodeGraph() --> Graph # Returns the clip's node graph object. + GetColorGroup() --> ColorGroup # Returns the clip's color group if one exists. + AssignToColorGroup(ColorGroup) --> Bool # Returns True if TiItem to successfully assigned to given ColorGroup. ColorGroup must be an existing group in the current project. + RemoveFromColorGroup() --> Bool # Returns True if the TiItem is successfully removed from the ColorGroup it is in. + ExportLUT(exportType, path) --> Bool # Exports LUTs from tiItem referring to value passed in 'exportType' (enum) for LUT size. Refer to. 'ExportLUT notes' section for possible values. + # Saves generated LUT in the provided 'path' (string). 'path' should include the intended file name. + # If an empty or incorrect extension is provided, the appropriate extension (.cube/.vlt) will be appended at the end of the path. Gallery GetAlbumName(galleryStillAlbum) --> string # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'. @@ -428,17 +451,63 @@ GalleryStillAlbum GetStills() --> [galleryStill] # Returns the list of GalleryStill objects in the album. GetLabel(galleryStill) --> string # Returns the label of the galleryStill. SetLabel(galleryStill, label) --> Bool # Sets the new 'label' to GalleryStill object 'galleryStill'. - ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm). + ImportStills([filePaths]) --> Bool # Imports GalleryStill from each filePath in [filePaths] list. True if at least one still is imported successfully. False otherwise. + ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm, drx). DeleteStills([galleryStill]) --> Bool # Deletes specified list of GalleryStill objects '[galleryStill]'. GalleryStill # This class does not provide any API functions but the object type is used by functions in other classes. +Graph + GetNumNodes() --> int # Returns the number of nodes in the graph + SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= self.GetNumNodes(). + # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). + # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). + GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes. + GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. + GetToolsInNode(nodeIndex) --> [toolsList] # Returns toolsList (list of strings) of the tools used in the node indicated by given nodeIndex (int). + +ColorGroup + GetName() --> String # Returns the name (string) of the ColorGroup. + SetName(groupName) --> Bool # Renames ColorGroup to groupName (string). + GetClipsInTimeline(Timeline=CurrTimeline) --> [TimelineItem] # Returns a list of TimelineItem that are in colorGroup in the given Timeline. Timeline is Current Timeline by default. + GetPreClipNodeGraph() --> Graph # Returns the ColorGroup Pre-clip graph. + GetPostClipNodeGraph() --> Graph # Returns the ColorGroup Post-clip graph. + List and Dict Data Structures ----------------------------- Beside primitive data types, Resolve's Python API mainly uses list and dict data structures. Lists are denoted by [ ... ] and dicts are denoted by { ... } above. As Lua does not support list and dict data structures, the Lua API implements "list" as a table with indices, e.g. { [1] = listValue1, [2] = listValue2, ... }. Similarly the Lua API implements "dict" as a table with the dictionary key as first element, e.g. { [dictKey1] = dictValue1, [dictKey2] = dictValue2, ... }. +Keyframe Mode information +------------------------- +This section covers additional notes for the functions Resolve.GetKeyframeMode() and Resolve.SetKeyframeMode(keyframeMode). + +'keyframeMode' can be one of the following enums: + - resolve.KEYFRAME_MODE_ALL == 0 + - resolve.KEYFRAME_MODE_COLOR == 1 + - resolve.KEYFRAME_MODE_SIZING == 2 + +Integer values returned by Resolve.GetKeyframeMode() will correspond to the enums above. + +Cloud Projects Settings +-------------------------------------- +This section covers additional notes for the functions "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" + +All three functions take in a {cloudSettings} dict, that have the following keys: +* resolve.CLOUD_SETTING_PROJECT_NAME: String, ["" by default] +* resolve.CLOUD_SETTING_PROJECT_MEDIA_PATH: String, ["" by default] +* resolve.CLOUD_SETTING_IS_COLLAB: Bool, [False by default] +* resolve.CLOUD_SETTING_SYNC_MODE: syncMode (see below), [resolve.CLOUD_SYNC_PROXY_ONLY by default] +* resolve.CLOUD_SETTING_IS_CAMERA_ACCESS: Bool [False by default] + +Where syncMode is one of the following values: +* resolve.CLOUD_SYNC_NONE, +* resolve.CLOUD_SYNC_PROXY_ONLY, +* resolve.CLOUD_SYNC_PROXY_AND_ORIG + +All three "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" require resolve.PROJECT_MEDIA_PATH to be defined. "ProjectManager:CreateCloudProject" also requires resolve.PROJECT_NAME to be defined. + Looking up Project and Clip properties -------------------------------------- This section covers additional notes for the functions "Project:GetSetting", "Project:SetSetting", "Timeline:GetSetting", "Timeline:SetSetting", "MediaPoolItem:GetClipProperty" and @@ -478,6 +547,49 @@ Affects: • x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x) • for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] +Auto Caption Settings +---------------------- +This section covers the supported settings for the method Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) + +The parameter setting is a dictionary containing the following keys: +* resolve.SUBTITLE_LANGUAGE: languageID (see below), [resolve.AUTO_CAPTION_AUTO by default] +* resolve.SUBTITLE_CAPTION_PRESET: presetType (see below), [resolve.AUTO_CAPTION_SUBTITLE_DEFAULT by default] +* resolve.SUBTITLE_CHARS_PER_LINE: Number between 1 and 60 inclusive [42 by default] +* resolve.SUBTITLE_LINE_BREAK: lineBreakType (see below), [resolve.AUTO_CAPTION_LINE_SINGLE by default] +* resolve.SUBTITLE_GAP: Number between 0 and 10 inclusive [0 by default] + +Note that the default values for some keys may change based on values defined for other keys, as per the UI. +For example, if the following dictionary is supplied, + CreateSubtitlesFromAudio( { resolve.SUBTITLE_LANGUAGE = resolve.AUTO_CAPTION_KOREAN, + resolve.SUBTITLE_CAPTION_PRESET = resolve.AUTO_CAPTION_NETFLIX } ) +the default value for resolve.SUBTITLE_CHARS_PER_LINE will be 16 instead of 42 + +languageIDs: +* resolve.AUTO_CAPTION_AUTO +* resolve.AUTO_CAPTION_DANISH +* resolve.AUTO_CAPTION_DUTCH +* resolve.AUTO_CAPTION_ENGLISH +* resolve.AUTO_CAPTION_FRENCH +* resolve.AUTO_CAPTION_GERMAN +* resolve.AUTO_CAPTION_ITALIAN +* resolve.AUTO_CAPTION_JAPANESE +* resolve.AUTO_CAPTION_KOREAN +* resolve.AUTO_CAPTION_MANDARIN_SIMPLIFIED +* resolve.AUTO_CAPTION_MANDARIN_TRADITIONAL +* resolve.AUTO_CAPTION_NORWEGIAN +* resolve.AUTO_CAPTION_PORTUGUESE +* resolve.AUTO_CAPTION_RUSSIAN +* resolve.AUTO_CAPTION_SPANISH +* resolve.AUTO_CAPTION_SWEDISH + +presetTypes: +* resolve.AUTO_CAPTION_SUBTITLE_DEFAULT +* resolve.AUTO_CAPTION_TELETEXT +* resolve.AUTO_CAPTION_NETFLIX + +lineBreakTypes: +* resolve.AUTO_CAPTION_LINE_SINGLE +* resolve.AUTO_CAPTION_LINE_DOUBLE Looking up Render Settings -------------------------- @@ -531,6 +643,8 @@ exportType can be one of the following constants: - resolve.EXPORT_DOLBY_VISION_VER_4_0 - resolve.EXPORT_DOLBY_VISION_VER_5_1 - resolve.EXPORT_OTIO + - resolve.EXPORT_ALE + - resolve.EXPORT_ALE_CDL exportSubtype can be one of the following enums: - resolve.EXPORT_NONE - resolve.EXPORT_AAF_NEW @@ -627,7 +741,8 @@ The supported keys with their accepted values are: - MOTION_EST_STANDARD_BETTER - MOTION_EST_ENHANCED_FASTER - MOTION_EST_ENHANCED_BETTER - - MOTION_EST_SPEED_WRAP + - MOTION_EST_SPEED_WARP_BETTER + - MOTION_EST_SPEED_WARP_FASTER "Scaling" : A value from the following constants - SCALE_USE_PROJECT = 0 - SCALE_CROP @@ -659,6 +774,16 @@ as a single argument. Getting the values for the keys that uses constants will return the number which is in the constant +ExportLUT notes +--------------- +The following section covers additional notes for TimelineItem.ExportLUT(exportType, path). + +Supported values for 'exportType' (enum) are: + - resolve.EXPORT_LUT_17PTCUBE + - resolve.EXPORT_LUT_33PTCUBE + - resolve.EXPORT_LUT_65PTCUBE + - resolve.EXPORT_LUT_PANASONICVLUT + Deprecated Resolve API Functions -------------------------------- The following API functions are deprecated. @@ -693,7 +818,12 @@ TimelineItem GetFusionCompNames() --> {names...} # Returns a dict of Fusion composition names associated with the timeline item. GetFlags() --> {colors...} # Returns a dict of flag colors assigned to the item. GetVersionNames(versionType) --> {names...} # Returns a dict of version names by provided versionType: 0 - local, 1 - remote. - + GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item + SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes. + # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). + # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). + GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes. + GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. Unsupported Resolve API Functions --------------------------------- From ee4be82e13eb3bfdc913e5c6f67cb392e97de4a9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 May 2024 13:48:05 +0200 Subject: [PATCH 02/65] package loader wip --- .../plugins/load/load_editorial_package.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py new file mode 100644 index 0000000000..fd1e53ea6c --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py @@ -0,0 +1,29 @@ +from ayon_core.pipeline import ( + load, + get_representation_path, +) + + + +class LoadEditorialPackage(load.LoaderPlugin): + """Load editorial package to timeline. + + Loading timeline from OTIO file included media sources + and timeline structure. + """ + + product_types = {"editorial_pckg"} + + representations = {"*"} + extensions = {"otio"} + + label = "Load as Timeline" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + # load clip to timeline and get main variables + files = get_representation_path(context["representation"]) + + print("Loading editorial package: ", files) From 2a35544a05dacb0ae86868d6d6b07e50ae340402 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 May 2024 14:14:26 +0200 Subject: [PATCH 03/65] loading otio as timeline --- .../plugins/load/load_editorial_package.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py index fd1e53ea6c..9b723b0130 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py @@ -1,8 +1,11 @@ +from pathlib import Path + from ayon_core.pipeline import ( load, get_representation_path, ) +from ayon_core.hosts.resolve.api import lib class LoadEditorialPackage(load.LoaderPlugin): @@ -19,11 +22,25 @@ class LoadEditorialPackage(load.LoaderPlugin): label = "Load as Timeline" order = -10 - icon = "code-fork" + icon = "ei.align-left" color = "orange" def load(self, context, name, namespace, data): - # load clip to timeline and get main variables files = get_representation_path(context["representation"]) - print("Loading editorial package: ", files) + search_folder_path = Path(files).parent / "resources" + + project = lib.get_current_project() + media_pool = project.GetMediaPool() + import_options = { + "timelineName": "Editorial Package Timeline", + "importSourceClips": True, + "sourceClipsPath": search_folder_path.as_posix(), + } + + timeline = media_pool.ImportTimelineFromFile(files, import_options) + print("Timeline imported: ", timeline) + + def update(self, container, context): + # TODO: implement update method in future + pass From 700967a28b86690cff6b95b8d8b771cbb8d552ac Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 16:06:10 +0800 Subject: [PATCH 04/65] move substance painter integration to server-addon and rename the folder as ayon_substancepainter --- .../client/ayon_substancepainter}/__init__.py | 0 .../client/ayon_substancepainter}/addon.py | 0 .../ayon_substancepainter}/api/__init__.py | 0 .../ayon_substancepainter}/api/colorspace.py | 0 .../client/ayon_substancepainter}/api/lib.py | 0 .../ayon_substancepainter/api/pipeline.py | 425 ++++++++++++ .../deploy/plugins/openpype_plugin.py | 0 .../startup/openpype_load_on_first_run.py | 0 .../plugins/create/create_textures.py | 0 .../plugins/create/create_workfile.py | 0 .../plugins/load/load_mesh.py | 0 .../plugins/publish/collect_current_file.py | 0 .../publish/collect_textureset_images.py | 0 .../collect_workfile_representation.py | 0 .../plugins/publish/extract_textures.py | 0 .../plugins/publish/increment_workfile.py | 0 .../plugins/publish/save_workfile.py | 0 .../plugins/publish/validate_ouput_maps.py | 0 .../client/substancepainter/__init__.py | 10 + .../client/substancepainter/addon.py | 31 + .../client/substancepainter/api/__init__.py | 8 + .../client/substancepainter/api/colorspace.py | 157 +++++ .../client/substancepainter/api/lib.py | 642 ++++++++++++++++++ .../client}/substancepainter/api/pipeline.py | 0 .../deploy/plugins/openpype_plugin.py | 36 + .../startup/openpype_load_on_first_run.py | 43 ++ .../plugins/create/create_textures.py | 175 +++++ .../plugins/create/create_workfile.py | 124 ++++ .../plugins/load/load_mesh.py | 246 +++++++ .../plugins/publish/collect_current_file.py | 17 + .../publish/collect_textureset_images.py | 211 ++++++ .../collect_workfile_representation.py | 26 + .../plugins/publish/extract_textures.py | 62 ++ .../plugins/publish/increment_workfile.py | 23 + .../plugins/publish/save_workfile.py | 28 + .../plugins/publish/validate_ouput_maps.py | 110 +++ 36 files changed, 2374 insertions(+) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/__init__.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/addon.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/api/__init__.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/api/colorspace.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/api/lib.py (100%) create mode 100644 server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/deploy/plugins/openpype_plugin.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/deploy/startup/openpype_load_on_first_run.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/create/create_textures.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/create/create_workfile.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/load/load_mesh.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/collect_current_file.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/collect_textureset_images.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/collect_workfile_representation.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/extract_textures.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/increment_workfile.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/save_workfile.py (100%) rename {client/ayon_core/hosts/substancepainter => server_addon/substancepainter/client/ayon_substancepainter}/plugins/publish/validate_ouput_maps.py (100%) create mode 100644 server_addon/substancepainter/client/substancepainter/__init__.py create mode 100644 server_addon/substancepainter/client/substancepainter/addon.py create mode 100644 server_addon/substancepainter/client/substancepainter/api/__init__.py create mode 100644 server_addon/substancepainter/client/substancepainter/api/colorspace.py create mode 100644 server_addon/substancepainter/client/substancepainter/api/lib.py rename {client/ayon_core/hosts => server_addon/substancepainter/client}/substancepainter/api/pipeline.py (100%) create mode 100644 server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py create mode 100644 server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py create mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py diff --git a/client/ayon_core/hosts/substancepainter/__init__.py b/server_addon/substancepainter/client/ayon_substancepainter/__init__.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/__init__.py rename to server_addon/substancepainter/client/ayon_substancepainter/__init__.py diff --git a/client/ayon_core/hosts/substancepainter/addon.py b/server_addon/substancepainter/client/ayon_substancepainter/addon.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/addon.py rename to server_addon/substancepainter/client/ayon_substancepainter/addon.py diff --git a/client/ayon_core/hosts/substancepainter/api/__init__.py b/server_addon/substancepainter/client/ayon_substancepainter/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/api/__init__.py rename to server_addon/substancepainter/client/ayon_substancepainter/api/__init__.py diff --git a/client/ayon_core/hosts/substancepainter/api/colorspace.py b/server_addon/substancepainter/client/ayon_substancepainter/api/colorspace.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/api/colorspace.py rename to server_addon/substancepainter/client/ayon_substancepainter/api/colorspace.py diff --git a/client/ayon_core/hosts/substancepainter/api/lib.py b/server_addon/substancepainter/client/ayon_substancepainter/api/lib.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/api/lib.py rename to server_addon/substancepainter/client/ayon_substancepainter/api/lib.py diff --git a/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py b/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py new file mode 100644 index 0000000000..6e0ca0145b --- /dev/null +++ b/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +"""Pipeline tools for OpenPype Substance Painter integration.""" +import os +import logging +from functools import partial + +# Substance 3D Painter modules +import substance_painter.ui +import substance_painter.event +import substance_painter.project + +import pyblish.api + +from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost +from ayon_core.settings import get_current_project_settings + +from ayon_core.pipeline.template_data import get_template_data_with_names +from ayon_core.pipeline import ( + register_creator_plugin_path, + register_loader_plugin_path, + AVALON_CONTAINER_ID, + Anatomy, +) +from ayon_core.lib import ( + StringTemplate, + register_event_callback, + emit_event, +) +from ayon_core.pipeline.load import any_outdated_containers +from ayon_substancepainter import SUBSTANCE_HOST_DIR + +from . import lib + +log = logging.getLogger("ayon_core.hosts.substance") + +PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +OPENPYPE_METADATA_KEY = "OpenPype" +OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key +OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key +OPENPYPE_METADATA_INSTANCES_KEY = "instances" # child key + + +class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): + name = "substancepainter" + + def __init__(self): + super(SubstanceHost, self).__init__() + self._has_been_setup = False + self.menu = None + self.callbacks = [] + self.shelves = [] + + def install(self): + pyblish.api.register_host("substancepainter") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + + log.info("Installing callbacks ... ") + # register_event_callback("init", on_init) + self._register_callbacks() + # register_event_callback("before.save", before_save) + # register_event_callback("save", on_save) + register_event_callback("open", on_open) + # register_event_callback("new", on_new) + + log.info("Installing menu ... ") + self._install_menu() + + project_settings = get_current_project_settings() + self._install_shelves(project_settings) + + self._has_been_setup = True + + def uninstall(self): + self._uninstall_shelves() + self._uninstall_menu() + self._deregister_callbacks() + + def workfile_has_unsaved_changes(self): + + if not substance_painter.project.is_open(): + return False + + return substance_painter.project.needs_saving() + + def get_workfile_extensions(self): + return [".spp", ".toc"] + + def save_workfile(self, dst_path=None): + + if not substance_painter.project.is_open(): + return False + + if not dst_path: + dst_path = self.get_current_workfile() + + full_save_mode = substance_painter.project.ProjectSaveMode.Full + substance_painter.project.save_as(dst_path, full_save_mode) + + return dst_path + + def open_workfile(self, filepath): + + if not os.path.exists(filepath): + raise RuntimeError("File does not exist: {}".format(filepath)) + + # We must first explicitly close current project before opening another + if substance_painter.project.is_open(): + substance_painter.project.close() + + substance_painter.project.open(filepath) + return filepath + + def get_current_workfile(self): + if not substance_painter.project.is_open(): + return None + + filepath = substance_painter.project.file_path() + if filepath and filepath.endswith(".spt"): + # When currently in a Substance Painter template assume our + # scene isn't saved. This can be the case directly after doing + # "New project", the path will then be the template used. This + # avoids Workfiles tool trying to save as .spt extension if the + # file hasn't been saved before. + return + + return filepath + + def get_containers(self): + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) + if containers: + for key, container in containers.items(): + container["objectName"] = key + yield container + + def update_context_data(self, data, changes): + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data) + + def get_context_data(self): + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {} + + def _install_menu(self): + from PySide2 import QtWidgets + from ayon_core.tools.utils import host_tools + + parent = substance_painter.ui.get_main_window() + + tab_menu_label = os.environ.get("AYON_MENU_LABEL") or "AYON" + menu = QtWidgets.QMenu(tab_menu_label) + + action = menu.addAction("Create...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent, + tab="create") + ) + + action = menu.addAction("Load...") + action.triggered.connect( + lambda: host_tools.show_loader(parent=parent, use_context=True) + ) + + action = menu.addAction("Publish...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent, + tab="publish") + ) + + action = menu.addAction("Manage...") + action.triggered.connect( + lambda: host_tools.show_scene_inventory(parent=parent) + ) + + action = menu.addAction("Library...") + action.triggered.connect( + lambda: host_tools.show_library_loader(parent=parent) + ) + + menu.addSeparator() + action = menu.addAction("Work Files...") + action.triggered.connect( + lambda: host_tools.show_workfiles(parent=parent) + ) + + substance_painter.ui.add_menu(menu) + + def on_menu_destroyed(): + self.menu = None + + menu.destroyed.connect(on_menu_destroyed) + + self.menu = menu + + def _uninstall_menu(self): + if self.menu: + self.menu.destroy() + self.menu = None + + def _register_callbacks(self): + # Prepare emit event callbacks + open_callback = partial(emit_event, "open") + + # Connect to the Substance Painter events + dispatcher = substance_painter.event.DISPATCHER + for event, callback in [ + (substance_painter.event.ProjectOpened, open_callback) + ]: + dispatcher.connect(event, callback) + # Keep a reference so we can deregister if needed + self.callbacks.append((event, callback)) + + def _deregister_callbacks(self): + for event, callback in self.callbacks: + substance_painter.event.DISPATCHER.disconnect(event, callback) + self.callbacks.clear() + + def _install_shelves(self, project_settings): + + shelves = project_settings["substancepainter"].get("shelves", []) + if not shelves: + return + + # Prepare formatting data if we detect any path which might have + # template tokens like {folder[name]} in there. + formatting_data = {} + has_formatting_entries = any("{" in item["value"] for item in shelves) + if has_formatting_entries: + project_name = self.get_current_project_name() + folder_path = self.get_current_folder_path() + task_name = self.get_current_task_name() + formatting_data = get_template_data_with_names( + project_name, folder_path, task_name, project_settings + ) + anatomy = Anatomy(project_name) + formatting_data["root"] = anatomy.roots + + for shelve_item in shelves: + + # Allow formatting with anatomy for the paths + path = shelve_item["value"] + if "{" in path: + path = StringTemplate.format_template(path, formatting_data) + + name = shelve_item["name"] + shelf_name = None + try: + shelf_name = lib.load_shelf(path, name=name) + except ValueError as exc: + print(f"Failed to load shelf -> {exc}") + + if shelf_name: + self.shelves.append(shelf_name) + + def _uninstall_shelves(self): + for shelf_name in self.shelves: + substance_painter.resource.Shelves.remove(shelf_name) + self.shelves.clear() + + +def on_open(): + log.info("Running callback on open..") + + if any_outdated_containers(): + from ayon_core.tools.utils import SimplePopup + + log.warning("Scene has outdated content.") + + # Get main window + parent = substance_painter.ui.get_main_window() + if parent is None: + log.info("Skipping outdated content pop-up " + "because Substance window can't be found.") + else: + + # Show outdated pop-up + def _on_show_inventory(): + from ayon_core.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = SimplePopup(parent=parent) + dialog.setWindowTitle("Substance scene has outdated content") + dialog.set_message("There are outdated containers in " + "your Substance scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() + + +def imprint_container(container, + name, + namespace, + context, + loader): + """Imprint a loaded container with metadata. + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + container (dict): The (substance metadata) dictionary to imprint into. + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (load.LoaderPlugin): loader instance used to produce container. + + Returns: + None + + """ + + data = [ + ("schema", "openpype:container-2.0"), + ("id", AVALON_CONTAINER_ID), + ("name", str(name)), + ("namespace", str(namespace) if namespace else None), + ("loader", str(loader.__class__.__name__)), + ("representation", context["representation"]["id"]), + ] + for key, value in data: + container[key] = value + + +def set_container_metadata(object_name, container_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + object_name (str): The unique object name identifier for the container + container_data (dict): The data for the container. + Note 'objectName' data is derived from `object_name` and key in + `container_data` will be ignored. + update (bool): Whether to only update the dict data. + + """ + # The objectName is derived from the key in the metadata so won't be stored + # in the metadata in the container's data. + container_data.pop("objectName", None) + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {} + if update: + existing_data = containers.setdefault(object_name, {}) + existing_data.update(container_data) # mutable dict, in-place update + else: + containers[object_name] = container_data + metadata.set("containers", containers) + + +def remove_container_metadata(object_name): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) + if containers: + containers.pop(object_name, None) + metadata.set("containers", containers) + + +def set_instance(instance_id, instance_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + instance_id (str): Unique identifier for the instance + instance_data (dict): The instance data to store in the metaadata. + """ + set_instances({instance_id: instance_data}, update=update) + + +def set_instances(instance_data_by_id, update=False): + """Store data for multiple instances at the same time. + + This is more optimal than querying and setting them in the metadata one + by one. + """ + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + for instance_id, instance_data in instance_data_by_id.items(): + if update: + existing_data = instances.get(instance_id, {}) + existing_data.update(instance_data) + else: + instances[instance_id] = instance_data + + metadata.set("instances", instances) + + +def remove_instance(instance_id): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + instances.pop(instance_id, None) + metadata.set("instances", instances) + + +def get_instances_by_id(): + """Return all instances stored in the project instances metadata""" + if not substance_painter.project.is_open(): + return {} + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + +def get_instances(): + """Return all instances stored in the project instances as a list""" + return list(get_instances_by_id().values()) diff --git a/client/ayon_core/hosts/substancepainter/deploy/plugins/openpype_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/deploy/plugins/openpype_plugin.py rename to server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py diff --git a/client/ayon_core/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/openpype_load_on_first_run.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py rename to server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/openpype_load_on_first_run.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/create/create_textures.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/create/create_textures.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/collect_current_file.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_current_file.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/collect_current_file.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_current_file.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/collect_workfile_representation.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_workfile_representation.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/collect_workfile_representation.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_workfile_representation.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/extract_textures.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/extract_textures.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/increment_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/increment_workfile.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/increment_workfile.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/increment_workfile.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/save_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/save_workfile.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/save_workfile.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/save_workfile.py diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/validate_ouput_maps.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/plugins/publish/validate_ouput_maps.py rename to server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/validate_ouput_maps.py diff --git a/server_addon/substancepainter/client/substancepainter/__init__.py b/server_addon/substancepainter/client/substancepainter/__init__.py new file mode 100644 index 0000000000..4c33b9f507 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + SubstanceAddon, + SUBSTANCE_HOST_DIR, +) + + +__all__ = ( + "SubstanceAddon", + "SUBSTANCE_HOST_DIR" +) diff --git a/server_addon/substancepainter/client/substancepainter/addon.py b/server_addon/substancepainter/client/substancepainter/addon.py new file mode 100644 index 0000000000..26829d3153 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/addon.py @@ -0,0 +1,31 @@ +import os +from ayon_core.addon import AYONAddon, IHostAddon + +SUBSTANCE_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class SubstanceAddon(AYONAddon, IHostAddon): + name = "substancepainter" + host_name = "substancepainter" + + def add_implementation_envs(self, env, _app): + # Add requirements to SUBSTANCE_PAINTER_PLUGINS_PATH + plugin_path = os.path.join(SUBSTANCE_HOST_DIR, "deploy") + plugin_path = plugin_path.replace("\\", "/") + if env.get("SUBSTANCE_PAINTER_PLUGINS_PATH"): + plugin_path += os.pathsep + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] + + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path + + # Log in Substance Painter doesn't support custom terminal colors + env["AYON_LOG_NO_COLORS"] = "1" + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(SUBSTANCE_HOST_DIR, "hooks") + ] + + def get_workfile_extensions(self): + return [".spp", ".toc"] diff --git a/server_addon/substancepainter/client/substancepainter/api/__init__.py b/server_addon/substancepainter/client/substancepainter/api/__init__.py new file mode 100644 index 0000000000..937d0c429e --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/api/__init__.py @@ -0,0 +1,8 @@ +from .pipeline import ( + SubstanceHost, + +) + +__all__ = [ + "SubstanceHost", +] diff --git a/server_addon/substancepainter/client/substancepainter/api/colorspace.py b/server_addon/substancepainter/client/substancepainter/api/colorspace.py new file mode 100644 index 0000000000..375b61b39b --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/api/colorspace.py @@ -0,0 +1,157 @@ +"""Substance Painter OCIO management + +Adobe Substance 3D Painter supports OCIO color management using a per project +configuration. Output color spaces are defined at the project level + +More information see: + - https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html # noqa + - https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html # noqa + +""" +import substance_painter.export +import substance_painter.js +import json + +from .lib import ( + get_document_structure, + get_channel_format +) + + +def _iter_document_stack_channels(): + """Yield all stack paths and channels project""" + + for material in get_document_structure()["materials"]: + material_name = material["name"] + for stack in material["stacks"]: + stack_name = stack["name"] + if stack_name: + stack_path = [material_name, stack_name] + else: + stack_path = material_name + for channel in stack["channels"]: + yield stack_path, channel + + +def _get_first_color_and_data_stack_and_channel(): + """Return first found color channel and data channel.""" + color_channel = None + data_channel = None + for stack_path, channel in _iter_document_stack_channels(): + channel_format = get_channel_format(stack_path, channel) + if channel_format["color"]: + color_channel = (stack_path, channel) + else: + data_channel = (stack_path, channel) + + if color_channel and data_channel: + return color_channel, data_channel + + return color_channel, data_channel + + +def get_project_channel_data(): + """Return colorSpace settings for the current substance painter project. + + In Substance Painter only color channels have Color Management enabled + whereas data channels have no color management applied. This can't be + changed. The artist can only customize the export color space for color + channels per bit-depth for 8 bpc, 16 bpc and 32 bpc. + + As such this returns the color space for 'data' and for per bit-depth + for color channels. + + Example output: + { + "data": {'colorSpace': 'Utility - Raw'}, + "8": {"colorSpace": "ACES - AcesCG"}, + "16": {"colorSpace": "ACES - AcesCG"}, + "16f": {"colorSpace": "ACES - AcesCG"}, + "32f": {"colorSpace": "ACES - AcesCG"} + } + + """ + + keys = ["colorSpace"] + query = {key: f"${key}" for key in keys} + + config = { + "exportPath": "/", + "exportShaderParams": False, + "defaultExportPreset": "query_preset", + + "exportPresets": [{ + "name": "query_preset", + + # List of maps making up this export preset. + "maps": [{ + "fileName": json.dumps(query), + # List of source/destination defining which channels will + # make up the texture file. + "channels": [], + "parameters": { + "fileFormat": "exr", + "bitDepth": "32f", + "dithering": False, + "sizeLog2": 4, + "paddingAlgorithm": "passthrough", + "dilationDistance": 16 + } + }] + }], + } + + def _get_query_output(config): + # Return the basename of the single output path we defined + result = substance_painter.export.list_project_textures(config) + path = next(iter(result.values()))[0] + # strip extension and slash since we know relevant json data starts + # and ends with { and } characters + path = path.strip("/\\.exr") + return json.loads(path) + + # Query for each type of channel (color and data) + color_channel, data_channel = _get_first_color_and_data_stack_and_channel() + colorspaces = {} + for key, channel_data in { + "data": data_channel, + "color": color_channel + }.items(): + if channel_data is None: + # No channel of that datatype anywhere in the Stack. We're + # unable to identify the output color space of the project + colorspaces[key] = None + continue + + stack, channel = channel_data + + # Stack must be a string + if not isinstance(stack, str): + # Assume iterable + stack = "/".join(stack) + + # Define the temp output config + config["exportList"] = [{"rootPath": stack}] + config_map = config["exportPresets"][0]["maps"][0] + config_map["channels"] = [ + { + "destChannel": x, + "srcChannel": x, + "srcMapType": "documentMap", + "srcMapName": channel + } for x in "RGB" + ] + + if key == "color": + # Query for each bit depth + # Color space definition can have a different OCIO config set + # for 8-bit, 16-bit and 32-bit outputs so we need to check each + # bit depth + for depth in ["8", "16", "16f", "32f"]: + config_map["parameters"]["bitDepth"] = depth # noqa + colorspaces[key + depth] = _get_query_output(config) + else: + # Data channel (not color managed) + colorspaces[key] = _get_query_output(config) + + return colorspaces diff --git a/server_addon/substancepainter/client/substancepainter/api/lib.py b/server_addon/substancepainter/client/substancepainter/api/lib.py new file mode 100644 index 0000000000..64c39943ce --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/api/lib.py @@ -0,0 +1,642 @@ +import os +import re +import json +from collections import defaultdict + +import substance_painter.project +import substance_painter.resource +import substance_painter.js +import substance_painter.export + +from qtpy import QtGui, QtWidgets, QtCore + + +def get_export_presets(): + """Return Export Preset resource URLs for all available Export Presets. + + Returns: + dict: {Resource url: GUI Label} + + """ + # TODO: Find more optimal way to find all export templates + + preset_resources = {} + for shelf in substance_painter.resource.Shelves.all(): + shelf_path = os.path.normpath(shelf.path()) + + presets_path = os.path.join(shelf_path, "export-presets") + if not os.path.exists(presets_path): + continue + + for filename in os.listdir(presets_path): + if filename.endswith(".spexp"): + template_name = os.path.splitext(filename)[0] + + resource = substance_painter.resource.ResourceID( + context=shelf.name(), + name=template_name + ) + resource_url = resource.url() + + preset_resources[resource_url] = template_name + + # Sort by template name + export_templates = dict(sorted(preset_resources.items(), + key=lambda x: x[1])) + + # Add default built-ins at the start + # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa + result = { + "export-preset-generator://viewport2d": "2D View", # noqa + "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa + "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa + "export-preset-generator://sketchfab": "Sketchfab", # noqa + "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa + "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa + "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa + "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa + } + result.update(export_templates) + return result + + +def _convert_stack_path_to_cmd_str(stack_path): + """Convert stack path `str` or `[str, str]` for javascript query + + Example usage: + >>> stack_path = _convert_stack_path_to_cmd_str(stack_path) + >>> cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + >>> substance_painter.js.evaluate(cmd) + + Args: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + str: Stack path usable as argument in javascript query. + + """ + return json.dumps(stack_path) + + +def get_channel_identifiers(stack_path=None): + """Return the list of channel identifiers. + + If a context is passed (texture set/stack), + return only used channels with resolved user channels. + + Channel identifiers are: + basecolor, height, specular, opacity, emissive, displacement, + glossiness, roughness, anisotropylevel, anisotropyangle, transmissive, + scattering, reflection, ior, metallic, normal, ambientOcclusion, + diffuse, specularlevel, blendingmask, [custom user names]. + + Args: + stack_path (list or str, Optional): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + list: List of channel identifiers. + + """ + if stack_path is None: + stack_path = "" + else: + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + return substance_painter.js.evaluate(cmd) + + +def get_channel_format(stack_path, channel): + """Retrieve the channel format of a specific stack channel. + + See `alg.mapexport.channelFormat` (javascript API) for more details. + + The channel format data is: + "label" (str): The channel format label: could be one of + [sRGB8, L8, RGB8, L16, RGB16, L16F, RGB16F, L32F, RGB32F] + "color" (bool): True if the format is in color, False is grayscale + "floating" (bool): True if the format uses floating point + representation, false otherwise + "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) + + Arguments: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + channel (str): Identifier of the channel to export + (see `get_channel_identifiers`) + + Returns: + dict: The channel format data. + + """ + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelFormat({stack_path}, '{channel}')" + return substance_painter.js.evaluate(cmd) + + +def get_document_structure(): + """Dump the document structure. + + See `alg.mapexport.documentStructure` (javascript API) for more details. + + Returns: + dict: Document structure or None when no project is open + + """ + return substance_painter.js.evaluate("alg.mapexport.documentStructure()") + + +def get_export_templates(config, format="png", strip_folder=True): + """Return export config outputs. + + This use the Javascript API `alg.mapexport.getPathsExportDocumentMaps` + which returns a different output than using the Python equivalent + `substance_painter.export.list_project_textures(config)`. + + The nice thing about the Javascript API version is that it returns the + output textures grouped by filename template. + + A downside is that it doesn't return all the UDIM tiles but per template + always returns a single file. + + Note: + The file format needs to be explicitly passed to the Javascript API + but upon exporting through the Python API the file format can be based + on the output preset. So it's likely the file extension will mismatch + + Warning: + Even though the function appears to solely get the expected outputs + the Javascript API will actually create the config's texture output + folder if it does not exist yet. As such, a valid path must be set. + + Example output: + { + "DefaultMaterial": { + "$textureSet_BaseColor(_$colorSpace)(.$udim)": "DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", # noqa + "$textureSet_Emissive(_$colorSpace)(.$udim)": "DefaultMaterial_Emissive_ACES - ACEScg.1002.png", # noqa + "$textureSet_Height(_$colorSpace)(.$udim)": "DefaultMaterial_Height_Utility - Raw.1002.png", # noqa + "$textureSet_Metallic(_$colorSpace)(.$udim)": "DefaultMaterial_Metallic_Utility - Raw.1002.png", # noqa + "$textureSet_Normal(_$colorSpace)(.$udim)": "DefaultMaterial_Normal_Utility - Raw.1002.png", # noqa + "$textureSet_Roughness(_$colorSpace)(.$udim)": "DefaultMaterial_Roughness_Utility - Raw.1002.png" # noqa + } + } + + Arguments: + config (dict) Export config + format (str, Optional): Output format to write to, defaults to 'png' + strip_folder (bool, Optional): Whether to strip the output folder + from the output filenames. + + Returns: + dict: The expected output maps. + + """ + folder = config["exportPath"].replace("\\", "/") + preset = config["defaultExportPreset"] + cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa + result = substance_painter.js.evaluate(cmd) + + if strip_folder: + for _stack, maps in result.items(): + for map_template, map_filepath in maps.items(): + map_filepath = map_filepath.replace("\\", "/") + assert map_filepath.startswith(folder) + map_filename = map_filepath[len(folder):].lstrip("/") + maps[map_template] = map_filename + + return result + + +def _templates_to_regex(templates, + texture_set, + colorspaces, + project, + mesh): + """Return regex based on a Substance Painter expot filename template. + + This converts Substance Painter export filename templates like + `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` into a regex + which can be used to query an output filename to help retrieve: + + - Which template filename the file belongs to. + - Which color space the file is written with. + - Which udim tile it is exactly. + + This is used by `get_parsed_export_maps` which tries to as explicitly + as possible match the filename pattern against the known possible outputs. + That's why Texture Set name, Color spaces, Project path and mesh path must + be provided. By doing so we get the best shot at correctly matching the + right template because otherwise $texture_set could basically be any string + and thus match even that of a color space or mesh. + + Arguments: + templates (list): List of templates to convert to regex. + texture_set (str): The texture set to match against. + colorspaces (list): The colorspaces defined in the current project. + project (str): Filepath of current substance project. + mesh (str): Path to mesh file used in current project. + + Returns: + dict: Template: Template regex pattern + + """ + def _filename_no_ext(path): + return os.path.splitext(os.path.basename(path))[0] + + if colorspaces and any(colorspaces): + colorspace_match = "|".join(re.escape(c) for c in set(colorspaces)) + colorspace_match = f"({colorspace_match})" + else: + # No colorspace support enabled + colorspace_match = "" + + # Key to regex valid search values + key_matches = { + "$project": re.escape(_filename_no_ext(project)), + "$mesh": re.escape(_filename_no_ext(mesh)), + "$textureSet": re.escape(texture_set), + "$colorSpace": colorspace_match, + "$udim": "([0-9]{4})" + } + + # Turn the templates into regexes + regexes = {} + for template in templates: + + # We need to tweak a temp + search_regex = re.escape(template) + + # Let's assume that any ( and ) character in the file template was + # intended as an optional template key and do a simple `str.replace` + # Note: we are matching against re.escape(template) so will need to + # search for the escaped brackets. + search_regex = search_regex.replace(re.escape("("), "(") + search_regex = search_regex.replace(re.escape(")"), ")?") + + # Substitute each key into a named group + for key, key_expected_regex in key_matches.items(): + + # We want to use the template as a regex basis in the end so will + # escape the whole thing first. Note that thus we'll need to + # search for the escaped versions of the keys too. + escaped_key = re.escape(key) + key_label = key[1:] # key without $ prefix + + key_expected_grp_regex = f"(?P<{key_label}>{key_expected_regex})" + search_regex = search_regex.replace(escaped_key, + key_expected_grp_regex) + + # The filename templates don't include the extension so we add it + # to be able to match the out filename beginning to end + ext_regex = r"(?P\.[A-Za-z][A-Za-z0-9-]*)" + search_regex = rf"^{search_regex}{ext_regex}$" + + regexes[template] = search_regex + + return regexes + + +def strip_template(template, strip="._ "): + """Return static characters in a substance painter filename template. + + >>> strip_template("$textureSet_HELLO(.$udim)") + # HELLO + >>> strip_template("$mesh_$textureSet_HELLO_WORLD_$colorSpace(.$udim)") + # HELLO_WORLD + >>> strip_template("$textureSet_HELLO(.$udim)", strip=None) + # _HELLO + >>> strip_template("$mesh_$textureSet_$colorSpace(.$udim)", strip=None) + # _HELLO_ + >>> strip_template("$textureSet_HELLO(.$udim)") + # _HELLO + + Arguments: + template (str): Filename template to strip. + strip (str, optional): Characters to strip from beginning and end + of the static string in template. Defaults to: `._ `. + + Returns: + str: The static string in filename template. + + """ + # Return only characters that were part of the template that were static. + # Remove all keys + keys = ["$project", "$mesh", "$textureSet", "$udim", "$colorSpace"] + stripped_template = template + for key in keys: + stripped_template = stripped_template.replace(key, "") + + # Everything inside an optional bracket space is excluded since it's not + # static. We keep a counter to track whether we are currently iterating + # over parts of the template that are inside an 'optional' group or not. + counter = 0 + result = "" + for char in stripped_template: + if char == "(": + counter += 1 + elif char == ")": + counter -= 1 + if counter < 0: + counter = 0 + else: + if counter == 0: + result += char + + if strip: + # Strip of any trailing start/end characters. Technically these are + # static but usually start and end separators like space or underscore + # aren't wanted. + result = result.strip(strip) + + return result + + +def get_parsed_export_maps(config): + """Return Export Config's expected output textures with parsed data. + + This tries to parse the texture outputs using a Python API export config. + + Parses template keys: $project, $mesh, $textureSet, $colorSpace, $udim + + Example: + {("DefaultMaterial", ""): { + "$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)": [ + { + // OUTPUT DATA FOR FILE #1 OF THE TEMPLATE + }, + { + // OUTPUT DATA FOR FILE #2 OF THE TEMPLATE + }, + ] + }, + }} + + File output data (all outputs are `str`). + 1) Parsed tokens: These are parsed tokens from the template, they will + only exist if found in the filename template and output filename. + + project: Workfile filename without extension + mesh: Filename of the loaded mesh without extension + textureSet: The texture set, e.g. "DefaultMaterial", + colorSpace: The color space, e.g. "ACES - ACEScg", + udim: The udim tile, e.g. "1001" + + 2) Template output and filepath + + filepath: Full path to the resulting texture map, e.g. + "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", + output: "mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png" + Note: if template had slashes (folders) then `output` will too. + So `output` might include a folder. + + Returns: + dict: [texture_set, stack]: {template: [file1_data, file2_data]} + + """ + # Import is here to avoid recursive lib <-> colorspace imports + from .colorspace import get_project_channel_data + + outputs = substance_painter.export.list_project_textures(config) + templates = get_export_templates(config, strip_folder=False) + + # Get all color spaces set for the current project + project_colorspaces = set( + data["colorSpace"] for data in get_project_channel_data().values() + ) + + # Get current project mesh path and project path to explicitly match + # the $mesh and $project tokens + project_mesh_path = substance_painter.project.last_imported_mesh_path() + project_path = substance_painter.project.file_path() + + # Get the current export path to strip this of the beginning of filepath + # results, since filename templates don't have these we'll match without + # that part of the filename. + export_path = config["exportPath"] + export_path = export_path.replace("\\", "/") + if not export_path.endswith("/"): + export_path += "/" + + # Parse the outputs + result = {} + for key, filepaths in outputs.items(): + texture_set, stack = key + + if stack: + stack_path = f"{texture_set}/{stack}" + else: + stack_path = texture_set + + stack_templates = list(templates[stack_path].keys()) + + template_regex = _templates_to_regex(stack_templates, + texture_set=texture_set, + colorspaces=project_colorspaces, + mesh=project_mesh_path, + project=project_path) + + # Let's precompile the regexes + for template, regex in template_regex.items(): + template_regex[template] = re.compile(regex) + + stack_results = defaultdict(list) + for filepath in sorted(filepaths): + # We strip explicitly using the full parent export path instead of + # using `os.path.basename` because export template is allowed to + # have subfolders in its template which we want to match against + filepath = filepath.replace("\\", "/") + assert filepath.startswith(export_path), ( + f"Filepath {filepath} must start with folder {export_path}" + ) + filename = filepath[len(export_path):] + + for template, regex in template_regex.items(): + match = regex.match(filename) + if match: + parsed = match.groupdict(default={}) + + # Include some special outputs for convenience + parsed["filepath"] = filepath + parsed["output"] = filename + + stack_results[template].append(parsed) + break + else: + raise ValueError(f"Unable to match {filename} against any " + f"template in: {list(template_regex.keys())}") + + result[key] = dict(stack_results) + + return result + + +def load_shelf(path, name=None): + """Add shelf to substance painter (for current application session) + + This will dynamically add a Shelf for the current session. It's good + to note however that these will *not* persist on restart of the host. + + Note: + Consider the loaded shelf a static library of resources. + + The shelf will *not* be visible in application preferences in + Edit > Settings > Libraries. + + The shelf will *not* show in the Assets browser if it has no existing + assets + + The shelf will *not* be a selectable option for selecting it as a + destination to import resources too. + + """ + + # Ensure expanded path with forward slashes + path = os.path.expandvars(path) + path = os.path.abspath(path) + path = path.replace("\\", "/") + + # Path must exist + if not os.path.isdir(path): + raise ValueError(f"Path is not an existing folder: {path}") + + # This name must be unique and must only contain lowercase letters, + # numbers, underscores or hyphens. + if name is None: + name = os.path.basename(path) + + name = name.lower() + name = re.sub(r"[^a-z0-9_\-]", "_", name) # sanitize to underscores + + if substance_painter.resource.Shelves.exists(name): + shelf = next( + shelf for shelf in substance_painter.resource.Shelves.all() + if shelf.name() == name + ) + if os.path.normpath(shelf.path()) != os.path.normpath(path): + raise ValueError(f"Shelf with name '{name}' already exists " + f"for a different path: '{shelf.path()}") + + return + + print(f"Adding Shelf '{name}' to path: {path}") + substance_painter.resource.Shelves.add(name, path) + + return name + + +def _get_new_project_action(): + """Return QAction which triggers Substance Painter's new project dialog""" + + main_window = substance_painter.ui.get_main_window() + + # Find the file menu's New file action + menubar = main_window.menuBar() + new_action = None + for action in menubar.actions(): + menu = action.menu() + if not menu: + continue + + if menu.objectName() != "file": + continue + + # Find the action with the CTRL+N key sequence + new_action = next(action for action in menu.actions() + if action.shortcut() == QtGui.QKeySequence.New) + break + + return new_action + + +def prompt_new_file_with_mesh(mesh_filepath): + """Prompts the user for a new file using Substance Painter's own dialog. + + This will set the mesh path to load to the given mesh and disables the + dialog box to disallow the user to change the path. This way we can allow + user configuration of a project but set the mesh path ourselves. + + Warning: + This is very hacky and experimental. + + Note: + If a project is currently open using the same mesh filepath it can't + accurately detect whether the user had actually accepted the new project + dialog or whether the project afterwards is still the original project, + for example when the user might have cancelled the operation. + + """ + + app = QtWidgets.QApplication.instance() + assert os.path.isfile(mesh_filepath), \ + f"Mesh filepath does not exist: {mesh_filepath}" + + def _setup_file_dialog(): + """Set filepath in QFileDialog and trigger accept result""" + file_dialog = app.activeModalWidget() + assert isinstance(file_dialog, QtWidgets.QFileDialog) + + # Quickly hide the dialog + file_dialog.hide() + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) + + file_dialog.setDirectory(os.path.dirname(mesh_filepath)) + url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) + file_dialog.selectUrl(url) + # TODO: find a way to improve the process event to + # load more complicated mesh + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 3000) + file_dialog.done(file_dialog.Accepted) + app.processEvents(QtCore.QEventLoop.AllEvents) + + def _setup_prompt(): + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + dialog = app.activeModalWidget() + assert dialog.objectName() == "NewProjectDialog" + + # Set the window title + mesh = os.path.basename(mesh_filepath) + dialog.setWindowTitle(f"New Project with mesh: {mesh}") + + # Get the select mesh file button + mesh_select = dialog.findChild(QtWidgets.QPushButton, "meshSelect") + + # Hide the select mesh button to the user to block changing of mesh + mesh_select.setVisible(False) + + # Ensure UI is visually up-to-date + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000) + + # Trigger the 'select file' dialog to set the path and have the + # new file dialog to use the path. + QtCore.QTimer.singleShot(10, _setup_file_dialog) + mesh_select.click() + + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + mesh_filename = dialog.findChild(QtWidgets.QFrame, "meshFileName") + mesh_filename_label = mesh_filename.findChild(QtWidgets.QLabel) + if not mesh_filename_label.text(): + dialog.close() + substance_painter.logging.warning( + "Failed to set mesh path with the prompt dialog:" + f"{mesh_filepath}\n\n" + "Creating new project directly with the mesh path instead.") + + new_action = _get_new_project_action() + if not new_action: + raise RuntimeError("Unable to detect new file action..") + + QtCore.QTimer.singleShot(0, _setup_prompt) + new_action.trigger() + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + if not substance_painter.project.is_open(): + return + + # Confirm mesh was set as expected + project_mesh = substance_painter.project.last_imported_mesh_path() + if os.path.normpath(project_mesh) != os.path.normpath(mesh_filepath): + return + + return project_mesh diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/server_addon/substancepainter/client/substancepainter/api/pipeline.py similarity index 100% rename from client/ayon_core/hosts/substancepainter/api/pipeline.py rename to server_addon/substancepainter/client/substancepainter/api/pipeline.py diff --git a/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py b/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py new file mode 100644 index 0000000000..8ced463367 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py @@ -0,0 +1,36 @@ + + +def cleanup_openpype_qt_widgets(): + """ + Workaround for Substance failing to shut down correctly + when a Qt window was still open at the time of shutting down. + + This seems to work sometimes, but not all the time. + + """ + # TODO: Create a more reliable method to close down all OpenPype Qt widgets + from PySide2 import QtWidgets + import substance_painter.ui + + # Kill OpenPype Qt widgets + print("Killing OpenPype Qt widgets..") + for widget in QtWidgets.QApplication.topLevelWidgets(): + if widget.__module__.startswith("openpype."): + print(f"Deleting widget: {widget.__class__.__name__}") + substance_painter.ui.delete_ui_element(widget) + + +def start_plugin(): + from ayon_core.pipeline import install_host + from ayon_core.hosts.substancepainter.api import SubstanceHost + install_host(SubstanceHost()) + + +def close_plugin(): + from ayon_core.pipeline import uninstall_host + cleanup_openpype_qt_widgets() + uninstall_host() + + +if __name__ == "__main__": + start_plugin() diff --git a/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py b/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py new file mode 100644 index 0000000000..04b610b4df --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py @@ -0,0 +1,43 @@ +"""Ease the OpenPype on-boarding process by loading the plug-in on first run""" + +OPENPYPE_PLUGIN_NAME = "openpype_plugin" + + +def start_plugin(): + try: + # This isn't exposed in the official API so we keep it in a try-except + from painter_plugins_ui import ( + get_settings, + LAUNCH_AT_START_KEY, + ON_STATE, + PLUGINS_MENU, + plugin_manager + ) + + # The `painter_plugins_ui` plug-in itself is also a startup plug-in + # we need to take into account that it could run either earlier or + # later than this startup script, we check whether its menu initialized + is_before_plugins_menu = PLUGINS_MENU is None + + settings = get_settings(OPENPYPE_PLUGIN_NAME) + if settings.value(LAUNCH_AT_START_KEY, None) is None: + print("Initializing OpenPype plug-in on first run...") + if is_before_plugins_menu: + print("- running before 'painter_plugins_ui'") + # Delay the launch to the painter_plugins_ui initialization + settings.setValue(LAUNCH_AT_START_KEY, ON_STATE) + else: + # Launch now + print("- running after 'painter_plugins_ui'") + plugin_manager(OPENPYPE_PLUGIN_NAME)(True) + + # Set the checked state in the menu to avoid confusion + action = next(action for action in PLUGINS_MENU._menu.actions() + if action.text() == OPENPYPE_PLUGIN_NAME) + if action is not None: + action.blockSignals(True) + action.setChecked(True) + action.blockSignals(False) + + except Exception as exc: + print(exc) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py b/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py new file mode 100644 index 0000000000..f46afadb5a --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating textures.""" + +from ayon_core.pipeline import CreatedInstance, Creator, CreatorError +from ayon_core.lib import ( + EnumDef, + UILabelDef, + NumberDef, + BoolDef +) + +from ayon_core.hosts.substancepainter.api.pipeline import ( + get_instances, + set_instance, + set_instances, + remove_instance +) +from ayon_core.hosts.substancepainter.api.lib import get_export_presets + +import substance_painter.project + + +class CreateTextures(Creator): + """Create a texture set.""" + identifier = "io.openpype.creators.substancepainter.textureset" + label = "Textures" + product_type = "textureSet" + icon = "picture-o" + + default_variant = "Main" + + def create(self, product_name, instance_data, pre_create_data): + + if not substance_painter.project.is_open(): + raise CreatorError("Can't create a Texture Set instance without " + "an open project.") + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in [ + "exportPresetUrl", + "exportFileFormat", + "exportSize", + "exportPadding", + "exportDilationDistance" + ]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] + + instance = self.create_instance_in_context(product_name, + instance_data) + set_instance( + instance_id=instance["instance_id"], + instance_data=instance.data_to_store() + ) + + def collect_instances(self): + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("productType") == self.product_type): + self.create_instance_in_context_from_existing(instance) + + def update_instances(self, update_list): + instance_data_by_id = {} + for instance, _changes in update_list: + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) + + def remove_instances(self, instances): + for instance in instances: + remove_instance(instance["instance_id"]) + self._remove_instance_from_context(instance) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, product_name, data): + instance = CreatedInstance( + self.product_type, product_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance + + def get_instance_attr_defs(self): + + return [ + EnumDef("exportPresetUrl", + items=get_export_presets(), + label="Output Template"), + BoolDef("allowSkippedMaps", + label="Allow Skipped Output Maps", + tooltip="When enabled this allows the publish to ignore " + "output maps in the used output template if one " + "or more maps are skipped due to the required " + "channels not being present in the current file.", + default=True), + EnumDef("exportFileFormat", + items={ + None: "Based on output template", + # TODO: Get available extensions from substance API + "bmp": "bmp", + "ico": "ico", + "jpeg": "jpeg", + "jng": "jng", + "pbm": "pbm", + "pgm": "pgm", + "png": "png", + "ppm": "ppm", + "tga": "targa", + "tif": "tiff", + "wap": "wap", + "wbmp": "wbmp", + "xpm": "xpm", + "gif": "gif", + "hdr": "hdr", + "exr": "exr", + "j2k": "j2k", + "jp2": "jp2", + "pfm": "pfm", + "webp": "webp", + # TODO: Unsure why jxr format fails to export + # "jxr": "jpeg-xr", + # TODO: File formats that combine the exported textures + # like psd are not correctly supported due to + # publishing only a single file + # "psd": "psd", + # "sbsar": "sbsar", + }, + default=None, + label="File type"), + EnumDef("exportSize", + items={ + None: "Based on each Texture Set's size", + # The key is size of the texture file in log2. + # (i.e. 10 means 2^10 = 1024) + 7: "128", + 8: "256", + 9: "512", + 10: "1024", + 11: "2048", + 12: "4096", + 13: "8192" + }, + default=None, + label="Size"), + + EnumDef("exportPadding", + items={ + "passthrough": "No padding (passthrough)", + "infinite": "Dilation infinite", + "transparent": "Dilation + transparent", + "color": "Dilation + default background color", + "diffusion": "Dilation + diffusion" + }, + default="infinite", + label="Padding"), + NumberDef("exportDilationDistance", + minimum=0, + maximum=256, + decimals=0, + default=16, + label="Dilation Distance"), + UILabelDef("*only used with " + "'Dilation + ' padding"), + ] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return self.get_instance_attr_defs() diff --git a/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py new file mode 100644 index 0000000000..63b1c6c7da --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" + +import ayon_api + +from ayon_core.pipeline import CreatedInstance, AutoCreator + +from ayon_core.hosts.substancepainter.api.pipeline import ( + set_instances, + set_instance, + get_instances +) + +import substance_painter.project + + +class CreateWorkfile(AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.substancepainter.workfile" + label = "Workfile" + product_type = "workfile" + icon = "document" + + default_variant = "Main" + + def create(self): + + if not substance_painter.project.is_open(): + return + + variant = self.default_variant + project_name = self.project_name + folder_path = self.create_context.get_current_folder_path() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + + # Workfile instance should always exist and must only exist once. + # As such we'll first check if it already exists and is collected. + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), None) + + current_folder_path = None + if current_instance is not None: + current_folder_path = current_instance["folderPath"] + + if current_instance is None: + self.log.info("Auto-creating workfile instance...") + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + product_name = self.get_product_name( + project_name, + folder_entity, + task_entity, + variant, + host_name, + ) + data = { + "folderPath": folder_path, + "task": task_name, + "variant": variant + } + current_instance = self.create_instance_in_context(product_name, + data) + elif ( + current_folder_path != folder_path + or current_instance["task"] != task_name + ): + # Update instance context if is not the same + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + product_name = self.get_product_name( + project_name, + folder_entity, + task_entity, + variant, + host_name, + ) + current_instance["folderPath"] = folder_path + current_instance["task"] = task_name + current_instance["productName"] = product_name + + set_instance( + instance_id=current_instance.get("instance_id"), + instance_data=current_instance.data_to_store() + ) + + def collect_instances(self): + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("productType") == self.product_type): + self.create_instance_in_context_from_existing(instance) + + def update_instances(self, update_list): + instance_data_by_id = {} + for instance, _changes in update_list: + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, product_name, data): + instance = CreatedInstance( + self.product_type, product_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance diff --git a/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py b/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py new file mode 100644 index 0000000000..d5aac1191c --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py @@ -0,0 +1,246 @@ +import copy +from qtpy import QtWidgets, QtCore +from ayon_core.pipeline import ( + load, + get_representation_path, +) +from ayon_core.pipeline.load import LoadError +from ayon_core.hosts.substancepainter.api.pipeline import ( + imprint_container, + set_container_metadata, + remove_container_metadata +) + +import substance_painter.project + + +def _convert(substance_attr): + """Return Substance Painter Python API Project attribute from string. + + This converts a string like "ProjectWorkflow.Default" to for example + the Substance Painter Python API equivalent object, like: + `substance_painter.project.ProjectWorkflow.Default` + + Args: + substance_attr (str): The `substance_painter.project` attribute, + for example "ProjectWorkflow.Default" + + Returns: + Any: Substance Python API object of the project attribute. + + Raises: + ValueError: If attribute does not exist on the + `substance_painter.project` python api. + """ + root = substance_painter.project + for attr in substance_attr.split("."): + root = getattr(root, attr, None) + if root is None: + raise ValueError( + "Substance Painter project attribute" + f" does not exist: {substance_attr}") + + return root + + +def get_template_by_name(name: str, templates: list[dict]) -> dict: + return next( + template for template in templates + if template["name"] == name + ) + + +class SubstanceProjectConfigurationWindow(QtWidgets.QDialog): + """The pop-up dialog allows users to choose material + duplicate options for importing Max objects when updating + or switching assets. + """ + def __init__(self, project_templates): + super(SubstanceProjectConfigurationWindow, self).__init__() + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) + + self.configuration = None + self.template_names = [template["name"] for template + in project_templates] + self.project_templates = project_templates + + self.widgets = { + "label": QtWidgets.QLabel( + "Select your template for project configuration"), + "template_options": QtWidgets.QComboBox(), + "import_cameras": QtWidgets.QCheckBox("Import Cameras"), + "preserve_strokes": QtWidgets.QCheckBox("Preserve Strokes"), + "clickbox": QtWidgets.QWidget(), + "combobox": QtWidgets.QWidget(), + "buttons": QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok + | QtWidgets.QDialogButtonBox.Cancel) + } + + self.widgets["template_options"].addItems(self.template_names) + + template_name = self.widgets["template_options"].currentText() + self._update_to_match_template(template_name) + # Build clickboxes + layout = QtWidgets.QHBoxLayout(self.widgets["clickbox"]) + layout.addWidget(self.widgets["import_cameras"]) + layout.addWidget(self.widgets["preserve_strokes"]) + # Build combobox + layout = QtWidgets.QHBoxLayout(self.widgets["combobox"]) + layout.addWidget(self.widgets["template_options"]) + # Build buttons + layout = QtWidgets.QHBoxLayout(self.widgets["buttons"]) + # Build layout. + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.widgets["label"]) + layout.addWidget(self.widgets["combobox"]) + layout.addWidget(self.widgets["clickbox"]) + layout.addWidget(self.widgets["buttons"]) + + self.widgets["template_options"].currentTextChanged.connect( + self._update_to_match_template) + self.widgets["buttons"].accepted.connect(self.on_accept) + self.widgets["buttons"].rejected.connect(self.on_reject) + + def on_accept(self): + self.configuration = self.get_project_configuration() + self.close() + + def on_reject(self): + self.close() + + def _update_to_match_template(self, template_name): + template = get_template_by_name(template_name, self.project_templates) + self.widgets["import_cameras"].setChecked(template["import_cameras"]) + self.widgets["preserve_strokes"].setChecked( + template["preserve_strokes"]) + + def get_project_configuration(self): + templates = self.project_templates + template_name = self.widgets["template_options"].currentText() + template = get_template_by_name(template_name, templates) + template = copy.deepcopy(template) # do not edit the original + template["import_cameras"] = self.widgets["import_cameras"].isChecked() + template["preserve_strokes"] = ( + self.widgets["preserve_strokes"].isChecked() + ) + for key in ["normal_map_format", + "project_workflow", + "tangent_space_mode"]: + template[key] = _convert(template[key]) + return template + + @classmethod + def prompt(cls, templates): + dialog = cls(templates) + dialog.exec_() + configuration = dialog.configuration + dialog.deleteLater() + return configuration + + +class SubstanceLoadProjectMesh(load.LoaderPlugin): + """Load mesh for project""" + + product_types = {"*"} + representations = {"abc", "fbx", "obj", "gltf", "usd", "usda", "usdc"} + + label = "Load mesh" + order = -10 + icon = "code-fork" + color = "orange" + + # Defined via settings + project_templates = [] + + def load(self, context, name, namespace, options=None): + + # Get user inputs + result = SubstanceProjectConfigurationWindow.prompt( + self.project_templates) + if not result: + # cancelling loader action + return + if not substance_painter.project.is_open(): + # Allow to 'initialize' a new project + path = self.filepath_from_context(context) + sp_settings = substance_painter.project.Settings( + import_cameras=result["import_cameras"], + normal_map_format=result["normal_map_format"], + project_workflow=result["project_workflow"], + tangent_space_mode=result["tangent_space_mode"], + default_texture_resolution=result["default_texture_resolution"] + ) + settings = substance_painter.project.create( + mesh_file_path=path, settings=sp_settings + ) + else: + # Reload the mesh + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=result["import_cameras"], + preserve_strokes=result["preserve_strokes"]) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa + self.log.info("Reload succeeded") + else: + raise LoadError("Reload of mesh failed") + + path = self.filepath_from_context(context) + substance_painter.project.reload_mesh(path, + settings, + on_mesh_reload) + + # Store container + container = {} + project_mesh_object_name = "_ProjectMesh_" + imprint_container(container, + name=project_mesh_object_name, + namespace=project_mesh_object_name, + context=context, + loader=self) + + # We want store some options for updating to keep consistent behavior + # from the user's original choice. We don't store 'preserve_strokes' + # as we always preserve strokes on updates. + container["options"] = { + "import_cameras": result["import_cameras"], + } + + set_container_metadata(project_mesh_object_name, container) + + def switch(self, container, context): + self.update(container, context) + + def update(self, container, context): + repre_entity = context["representation"] + + path = get_representation_path(repre_entity) + + # Reload the mesh + container_options = container.get("options", {}) + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=container_options.get("import_cameras", True), + preserve_strokes=True + ) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: + self.log.info("Reload succeeded") + else: + raise LoadError("Reload of mesh failed") + + substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + + # Update container representation + object_name = container["objectName"] + update_data = {"representation": repre_entity["id"]} + set_container_metadata(object_name, update_data, update=True) + + def remove(self, container): + + # Remove OpenPype related settings about what model was loaded + # or close the project? + # TODO: This is likely best 'hidden' away to the user because + # this will leave the project's mesh unmanaged. + remove_container_metadata(container["objectName"]) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py new file mode 100644 index 0000000000..db0edafac0 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import pyblish.api + +from ayon_core.pipeline import registered_host + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.49 + label = "Current Workfile" + hosts = ["substancepainter"] + + def process(self, context): + host = registered_host() + path = host.get_current_workfile() + context.data["currentFile"] = path + self.log.debug(f"Current workfile: {path}") diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py new file mode 100644 index 0000000000..20aaa56993 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py @@ -0,0 +1,211 @@ +import os +import copy + +import pyblish.api +import ayon_api + +import substance_painter.textureset +from ayon_core.pipeline import publish +from ayon_core.hosts.substancepainter.api.lib import ( + get_parsed_export_maps, + strip_template +) +from ayon_core.pipeline.create import get_product_name + + +class CollectTextureSet(pyblish.api.InstancePlugin): + """Extract Textures using an output template config""" + # TODO: Production-test usage of color spaces + # TODO: Detect what source data channels end up in each file + + label = "Collect Texture Set images" + hosts = ["substancepainter"] + families = ["textureSet"] + order = pyblish.api.CollectorOrder + + def process(self, instance): + + config = self.get_export_config(instance) + project_name = instance.context.data["projectName"] + folder_entity = ayon_api.get_folder_by_path( + project_name, + instance.data["folderPath"] + ) + task_name = instance.data.get("task") + task_entity = None + if folder_entity and task_name: + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + + instance.data["exportConfig"] = config + maps = get_parsed_export_maps(config) + + # Let's break the instance into multiple instances to integrate + # a product per generated texture or texture UDIM sequence + for (texture_set_name, stack_name), template_maps in maps.items(): + self.log.info(f"Processing {texture_set_name}/{stack_name}") + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs, + task_entity=task_entity, + texture_set_name=texture_set_name, + stack_name=stack_name) + + def create_image_instance(self, instance, template, outputs, + task_entity, texture_set_name, stack_name): + """Create a new instance per image or UDIM sequence. + + The new instances will be of product type `image`. + + """ + + context = instance.context + first_filepath = outputs[0]["filepath"] + fnames = [os.path.basename(output["filepath"]) for output in outputs] + ext = os.path.splitext(first_filepath)[1] + assert ext.lstrip("."), f"No extension: {ext}" + + always_include_texture_set_name = False # todo: make this configurable + all_texture_sets = substance_painter.textureset.all_texture_sets() + texture_set = substance_painter.textureset.TextureSet.from_name( + texture_set_name + ) + + # Define the suffix we want to give this particular texture + # set and set up a remapped product naming for it. + suffix = "" + if always_include_texture_set_name or len(all_texture_sets) > 1: + # More than one texture set, include texture set name + suffix += f".{texture_set_name}" + if texture_set.is_layered_material() and stack_name: + # More than one stack, include stack name + suffix += f".{stack_name}" + + # Always include the map identifier + map_identifier = strip_template(template) + suffix += f".{map_identifier}" + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + image_product_name = get_product_name( + # TODO: The product type actually isn't 'texture' currently but + # for now this is only done so the product name starts with + # 'texture' + context.data["projectName"], + task_name, + task_type, + context.data["hostName"], + product_type="texture", + variant=instance.data["variant"] + suffix, + project_settings=context.data["project_settings"] + ) + + # Prepare representation + representation = { + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": fnames if len(fnames) > 1 else fnames[0], + } + + # Mark as UDIM explicitly if it has UDIM tiles. + if bool(outputs[0].get("udim")): + # The representation for a UDIM sequence should have a `udim` key + # that is a list of all udim tiles (str) like: ["1001", "1002"] + # strings. See CollectTextures plug-in and Integrators. + representation["udim"] = [output["udim"] for output in outputs] + + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + representation["tags"] = ["review"] + representation["stagingDir"] = staging_dir + + # Clone the instance + product_type = "image" + image_instance = context.create_instance(image_product_name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(dict(instance.data))) + image_instance.data["name"] = image_product_name + image_instance.data["label"] = image_product_name + image_instance.data["productName"] = image_product_name + image_instance.data["productType"] = product_type + image_instance.data["family"] = product_type + image_instance.data["families"] = [product_type, "textures"] + image_instance.data["representations"] = [representation] + + # Group the textures together in the loader + image_instance.data["productGroup"] = image_product_name + + # Store the texture set name and stack name on the instance + image_instance.data["textureSetName"] = texture_set_name + image_instance.data["textureStackName"] = stack_name + + # Store color space with the instance + # Note: The extractor will assign it to the representation + colorspace = outputs[0].get("colorSpace") + if colorspace: + self.log.debug(f"{image_product_name} colorspace: {colorspace}") + image_instance.data["colorspace"] = colorspace + + # Store the instance in the original instance as a member + instance.append(image_instance) + + def get_export_config(self, instance): + """Return an export configuration dict for texture exports. + + This config can be supplied to: + - `substance_painter.export.export_project_textures` + - `substance_painter.export.list_project_textures` + + See documentation on substance_painter.export module about the + formatting of the configuration dictionary. + + Args: + instance (pyblish.api.Instance): Texture Set instance to be + published. + + Returns: + dict: Export config + + """ + + creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] + self.log.debug(f"Exporting using preset: {preset_url}") + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa + config = { # noqa + "exportShaderParams": True, + "exportPath": publish.get_instance_staging_dir(instance), + "defaultExportPreset": preset_url, + + # Custom overrides to the exporter + "exportParameters": [ + { + "parameters": { + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + } + } + ] + } + + # Create the list of Texture Sets to export. + config["exportList"] = [] + for texture_set in substance_painter.textureset.all_texture_sets(): + config["exportList"].append({"rootPath": texture_set.name()}) + + # Consider None values from the creator attributes optionals + for override in config["exportParameters"]: + parameters = override.get("parameters") + for key, value in dict(parameters).items(): + if value is None: + parameters.pop(key) + + return config diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py new file mode 100644 index 0000000000..8d98d0b014 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py @@ -0,0 +1,26 @@ +import os +import pyblish.api + + +class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): + """Create a publish representation for the current workfile instance.""" + + order = pyblish.api.CollectorOrder + label = "Workfile representation" + hosts = ["substancepainter"] + families = ["workfile"] + + def process(self, instance): + + context = instance.context + current_file = context.data["currentFile"] + + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data["representations"] = [{ + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": file, + "stagingDir": folder, + }] diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py new file mode 100644 index 0000000000..0fa7b52f45 --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py @@ -0,0 +1,62 @@ +import substance_painter.export + +from ayon_core.pipeline import KnownPublishError, publish + + +class ExtractTextures(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): + """Extract Textures using an output template config. + + Note: + This Extractor assumes that `collect_textureset_images` has prepared + the relevant export config and has also collected the individual image + instances for publishing including its representation. That is why this + particular Extractor doesn't specify representations to integrate. + + """ + + label = "Extract Texture Set" + hosts = ["substancepainter"] + families = ["textureSet"] + + # Run before thumbnail extractors + order = publish.Extractor.order - 0.1 + + def process(self, instance): + + config = instance.data["exportConfig"] + result = substance_painter.export.export_project_textures(config) + + if result.status != substance_painter.export.ExportStatus.Success: + raise KnownPublishError( + "Failed to export texture set: {}".format(result.message) + ) + + # Log what files we generated + for (texture_set_name, stack_name), maps in result.textures.items(): + # Log our texture outputs + self.log.info(f"Exported stack: {texture_set_name} {stack_name}") + for texture_map in maps: + self.log.info(f"Exported texture: {texture_map}") + + # We'll insert the color space data for each image instance that we + # added into this texture set. The collector couldn't do so because + # some anatomy and other instance data needs to be collected prior + context = instance.context + for image_instance in instance: + representation = next(iter(image_instance.data["representations"])) + + colorspace = image_instance.data.get("colorspace") + if not colorspace: + self.log.debug("No color space data present for instance: " + f"{image_instance}") + continue + + self.set_representation_colorspace(representation, + context=context, + colorspace=colorspace) + + # The TextureSet instance should not be integrated. It generates no + # output data. Instead the separated texture instances are generated + # from it which themselves integrate into the database. + instance.data["integrate"] = False diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py new file mode 100644 index 0000000000..521a28130b --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py @@ -0,0 +1,23 @@ +import pyblish.api + +from ayon_core.lib import version_up +from ayon_core.pipeline import registered_host + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 1 + label = "Increment Workfile Version" + optional = True + hosts = ["substancepainter"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not successful so version is not increased.") + + host = registered_host() + path = context.data["currentFile"] + self.log.info(f"Incrementing current workfile to: {path}") + host.save_workfile(version_up(path)) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py new file mode 100644 index 0000000000..627fb991aa --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py @@ -0,0 +1,28 @@ +import pyblish.api + +from ayon_core.pipeline import ( + registered_host, + KnownPublishError +) + + +class SaveCurrentWorkfile(pyblish.api.ContextPlugin): + """Save current workfile""" + + label = "Save current workfile" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["substancepainter"] + + def process(self, context): + + host = registered_host() + current = host.get_current_workfile() + if context.data["currentFile"] != current: + raise KnownPublishError("Workfile has changed during publishing!") + + if host.workfile_has_unsaved_changes(): + self.log.info("Saving current file: {}".format(current)) + host.save_workfile() + else: + self.log.debug("Skipping workfile save because there are no " + "unsaved changes.") diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py new file mode 100644 index 0000000000..720771994c --- /dev/null +++ b/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py @@ -0,0 +1,110 @@ +import copy +import os + +import pyblish.api + +import substance_painter.export + +from ayon_core.pipeline import PublishValidationError + + +class ValidateOutputMaps(pyblish.api.InstancePlugin): + """Validate all output maps for Output Template are generated. + + Output maps will be skipped by Substance Painter if it is an output + map in the Substance Output Template which uses channels that the current + substance painter project has not painted or generated. + + """ + + order = pyblish.api.ValidatorOrder + label = "Validate output maps" + hosts = ["substancepainter"] + families = ["textureSet"] + + def process(self, instance): + + config = instance.data["exportConfig"] + + # Substance Painter API does not allow to query the actual output maps + # it will generate without actually exporting the files. So we try to + # generate the smallest size / fastest export as possible + config = copy.deepcopy(config) + parameters = config["exportParameters"][0]["parameters"] + parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) + parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) + parameters["dithering"] = False # no dithering (faster) + + result = substance_painter.export.export_project_textures(config) + if result.status != substance_painter.export.ExportStatus.Success: + raise PublishValidationError( + "Failed to export texture set: {}".format(result.message) + ) + + generated_files = set() + for texture_maps in result.textures.values(): + for texture_map in texture_maps: + generated_files.add(os.path.normpath(texture_map)) + # Directly clean up our temporary export + os.remove(texture_map) + + creator_attributes = instance.data.get("creator_attributes", {}) + allow_skipped_maps = creator_attributes.get("allowSkippedMaps", True) + error_report_missing = [] + for image_instance in instance: + + # Confirm whether the instance has its expected files generated. + # We assume there's just one representation and that it is + # the actual texture representation from the collector. + representation = next(iter(image_instance.data["representations"])) + staging_dir = representation["stagingDir"] + filenames = representation["files"] + if not isinstance(filenames, (list, tuple)): + # Convert single file to list + filenames = [filenames] + + missing = [] + for filename in filenames: + filepath = os.path.join(staging_dir, filename) + filepath = os.path.normpath(filepath) + if filepath not in generated_files: + self.log.warning(f"Missing texture: {filepath}") + missing.append(filepath) + + if not missing: + continue + + if allow_skipped_maps: + # TODO: This is changing state on the instance's which + # should not be done during validation. + self.log.warning(f"Disabling texture instance: " + f"{image_instance}") + image_instance.data["active"] = False + image_instance.data["publish"] = False + image_instance.data["integrate"] = False + representation.setdefault("tags", []).append("delete") + continue + else: + error_report_missing.append((image_instance, missing)) + + if error_report_missing: + + message = ( + "The Texture Set skipped exporting some output maps which are " + "defined in the Output Template. This happens if the Output " + "Templates exports maps from channels which you do not " + "have in your current Substance Painter project.\n\n" + "To allow this enable the *Allow Skipped Output Maps* setting " + "on the instance.\n\n" + f"Instance {instance} skipped exporting output maps:\n" + "" + ) + + for image_instance, missing in error_report_missing: + missing_str = ", ".join(missing) + message += f"- **{image_instance}** skipped: {missing_str}\n" + + raise PublishValidationError( + message=message, + title="Missing output maps" + ) From c9d1998cab20f7b49798d27f7c2ff830fd92a71c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 16:06:38 +0800 Subject: [PATCH 05/65] move substance painter integration to server-addon and rename the folder as ayon_substancepainter --- .../client/substancepainter/__init__.py | 10 - .../client/substancepainter/addon.py | 31 - .../client/substancepainter/api/__init__.py | 8 - .../client/substancepainter/api/colorspace.py | 157 ----- .../client/substancepainter/api/lib.py | 642 ------------------ .../client/substancepainter/api/pipeline.py | 425 ------------ .../deploy/plugins/openpype_plugin.py | 36 - .../startup/openpype_load_on_first_run.py | 43 -- .../plugins/create/create_textures.py | 175 ----- .../plugins/create/create_workfile.py | 124 ---- .../plugins/load/load_mesh.py | 246 ------- .../plugins/publish/collect_current_file.py | 17 - .../publish/collect_textureset_images.py | 211 ------ .../collect_workfile_representation.py | 26 - .../plugins/publish/extract_textures.py | 62 -- .../plugins/publish/increment_workfile.py | 23 - .../plugins/publish/save_workfile.py | 28 - .../plugins/publish/validate_ouput_maps.py | 110 --- server_addon/substancepainter/package.py | 9 +- 19 files changed, 8 insertions(+), 2375 deletions(-) delete mode 100644 server_addon/substancepainter/client/substancepainter/__init__.py delete mode 100644 server_addon/substancepainter/client/substancepainter/addon.py delete mode 100644 server_addon/substancepainter/client/substancepainter/api/__init__.py delete mode 100644 server_addon/substancepainter/client/substancepainter/api/colorspace.py delete mode 100644 server_addon/substancepainter/client/substancepainter/api/lib.py delete mode 100644 server_addon/substancepainter/client/substancepainter/api/pipeline.py delete mode 100644 server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py delete mode 100644 server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py delete mode 100644 server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py diff --git a/server_addon/substancepainter/client/substancepainter/__init__.py b/server_addon/substancepainter/client/substancepainter/__init__.py deleted file mode 100644 index 4c33b9f507..0000000000 --- a/server_addon/substancepainter/client/substancepainter/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .addon import ( - SubstanceAddon, - SUBSTANCE_HOST_DIR, -) - - -__all__ = ( - "SubstanceAddon", - "SUBSTANCE_HOST_DIR" -) diff --git a/server_addon/substancepainter/client/substancepainter/addon.py b/server_addon/substancepainter/client/substancepainter/addon.py deleted file mode 100644 index 26829d3153..0000000000 --- a/server_addon/substancepainter/client/substancepainter/addon.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -from ayon_core.addon import AYONAddon, IHostAddon - -SUBSTANCE_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class SubstanceAddon(AYONAddon, IHostAddon): - name = "substancepainter" - host_name = "substancepainter" - - def add_implementation_envs(self, env, _app): - # Add requirements to SUBSTANCE_PAINTER_PLUGINS_PATH - plugin_path = os.path.join(SUBSTANCE_HOST_DIR, "deploy") - plugin_path = plugin_path.replace("\\", "/") - if env.get("SUBSTANCE_PAINTER_PLUGINS_PATH"): - plugin_path += os.pathsep + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] - - env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path - - # Log in Substance Painter doesn't support custom terminal colors - env["AYON_LOG_NO_COLORS"] = "1" - - def get_launch_hook_paths(self, app): - if app.host_name != self.host_name: - return [] - return [ - os.path.join(SUBSTANCE_HOST_DIR, "hooks") - ] - - def get_workfile_extensions(self): - return [".spp", ".toc"] diff --git a/server_addon/substancepainter/client/substancepainter/api/__init__.py b/server_addon/substancepainter/client/substancepainter/api/__init__.py deleted file mode 100644 index 937d0c429e..0000000000 --- a/server_addon/substancepainter/client/substancepainter/api/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .pipeline import ( - SubstanceHost, - -) - -__all__ = [ - "SubstanceHost", -] diff --git a/server_addon/substancepainter/client/substancepainter/api/colorspace.py b/server_addon/substancepainter/client/substancepainter/api/colorspace.py deleted file mode 100644 index 375b61b39b..0000000000 --- a/server_addon/substancepainter/client/substancepainter/api/colorspace.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Substance Painter OCIO management - -Adobe Substance 3D Painter supports OCIO color management using a per project -configuration. Output color spaces are defined at the project level - -More information see: - - https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html # noqa - - https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html # noqa - -""" -import substance_painter.export -import substance_painter.js -import json - -from .lib import ( - get_document_structure, - get_channel_format -) - - -def _iter_document_stack_channels(): - """Yield all stack paths and channels project""" - - for material in get_document_structure()["materials"]: - material_name = material["name"] - for stack in material["stacks"]: - stack_name = stack["name"] - if stack_name: - stack_path = [material_name, stack_name] - else: - stack_path = material_name - for channel in stack["channels"]: - yield stack_path, channel - - -def _get_first_color_and_data_stack_and_channel(): - """Return first found color channel and data channel.""" - color_channel = None - data_channel = None - for stack_path, channel in _iter_document_stack_channels(): - channel_format = get_channel_format(stack_path, channel) - if channel_format["color"]: - color_channel = (stack_path, channel) - else: - data_channel = (stack_path, channel) - - if color_channel and data_channel: - return color_channel, data_channel - - return color_channel, data_channel - - -def get_project_channel_data(): - """Return colorSpace settings for the current substance painter project. - - In Substance Painter only color channels have Color Management enabled - whereas data channels have no color management applied. This can't be - changed. The artist can only customize the export color space for color - channels per bit-depth for 8 bpc, 16 bpc and 32 bpc. - - As such this returns the color space for 'data' and for per bit-depth - for color channels. - - Example output: - { - "data": {'colorSpace': 'Utility - Raw'}, - "8": {"colorSpace": "ACES - AcesCG"}, - "16": {"colorSpace": "ACES - AcesCG"}, - "16f": {"colorSpace": "ACES - AcesCG"}, - "32f": {"colorSpace": "ACES - AcesCG"} - } - - """ - - keys = ["colorSpace"] - query = {key: f"${key}" for key in keys} - - config = { - "exportPath": "/", - "exportShaderParams": False, - "defaultExportPreset": "query_preset", - - "exportPresets": [{ - "name": "query_preset", - - # List of maps making up this export preset. - "maps": [{ - "fileName": json.dumps(query), - # List of source/destination defining which channels will - # make up the texture file. - "channels": [], - "parameters": { - "fileFormat": "exr", - "bitDepth": "32f", - "dithering": False, - "sizeLog2": 4, - "paddingAlgorithm": "passthrough", - "dilationDistance": 16 - } - }] - }], - } - - def _get_query_output(config): - # Return the basename of the single output path we defined - result = substance_painter.export.list_project_textures(config) - path = next(iter(result.values()))[0] - # strip extension and slash since we know relevant json data starts - # and ends with { and } characters - path = path.strip("/\\.exr") - return json.loads(path) - - # Query for each type of channel (color and data) - color_channel, data_channel = _get_first_color_and_data_stack_and_channel() - colorspaces = {} - for key, channel_data in { - "data": data_channel, - "color": color_channel - }.items(): - if channel_data is None: - # No channel of that datatype anywhere in the Stack. We're - # unable to identify the output color space of the project - colorspaces[key] = None - continue - - stack, channel = channel_data - - # Stack must be a string - if not isinstance(stack, str): - # Assume iterable - stack = "/".join(stack) - - # Define the temp output config - config["exportList"] = [{"rootPath": stack}] - config_map = config["exportPresets"][0]["maps"][0] - config_map["channels"] = [ - { - "destChannel": x, - "srcChannel": x, - "srcMapType": "documentMap", - "srcMapName": channel - } for x in "RGB" - ] - - if key == "color": - # Query for each bit depth - # Color space definition can have a different OCIO config set - # for 8-bit, 16-bit and 32-bit outputs so we need to check each - # bit depth - for depth in ["8", "16", "16f", "32f"]: - config_map["parameters"]["bitDepth"] = depth # noqa - colorspaces[key + depth] = _get_query_output(config) - else: - # Data channel (not color managed) - colorspaces[key] = _get_query_output(config) - - return colorspaces diff --git a/server_addon/substancepainter/client/substancepainter/api/lib.py b/server_addon/substancepainter/client/substancepainter/api/lib.py deleted file mode 100644 index 64c39943ce..0000000000 --- a/server_addon/substancepainter/client/substancepainter/api/lib.py +++ /dev/null @@ -1,642 +0,0 @@ -import os -import re -import json -from collections import defaultdict - -import substance_painter.project -import substance_painter.resource -import substance_painter.js -import substance_painter.export - -from qtpy import QtGui, QtWidgets, QtCore - - -def get_export_presets(): - """Return Export Preset resource URLs for all available Export Presets. - - Returns: - dict: {Resource url: GUI Label} - - """ - # TODO: Find more optimal way to find all export templates - - preset_resources = {} - for shelf in substance_painter.resource.Shelves.all(): - shelf_path = os.path.normpath(shelf.path()) - - presets_path = os.path.join(shelf_path, "export-presets") - if not os.path.exists(presets_path): - continue - - for filename in os.listdir(presets_path): - if filename.endswith(".spexp"): - template_name = os.path.splitext(filename)[0] - - resource = substance_painter.resource.ResourceID( - context=shelf.name(), - name=template_name - ) - resource_url = resource.url() - - preset_resources[resource_url] = template_name - - # Sort by template name - export_templates = dict(sorted(preset_resources.items(), - key=lambda x: x[1])) - - # Add default built-ins at the start - # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa - result = { - "export-preset-generator://viewport2d": "2D View", # noqa - "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa - "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa - "export-preset-generator://sketchfab": "Sketchfab", # noqa - "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa - "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa - "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa - "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa - } - result.update(export_templates) - return result - - -def _convert_stack_path_to_cmd_str(stack_path): - """Convert stack path `str` or `[str, str]` for javascript query - - Example usage: - >>> stack_path = _convert_stack_path_to_cmd_str(stack_path) - >>> cmd = f"alg.mapexport.channelIdentifiers({stack_path})" - >>> substance_painter.js.evaluate(cmd) - - Args: - stack_path (list or str): Path to the stack, could be - "Texture set name" or ["Texture set name", "Stack name"] - - Returns: - str: Stack path usable as argument in javascript query. - - """ - return json.dumps(stack_path) - - -def get_channel_identifiers(stack_path=None): - """Return the list of channel identifiers. - - If a context is passed (texture set/stack), - return only used channels with resolved user channels. - - Channel identifiers are: - basecolor, height, specular, opacity, emissive, displacement, - glossiness, roughness, anisotropylevel, anisotropyangle, transmissive, - scattering, reflection, ior, metallic, normal, ambientOcclusion, - diffuse, specularlevel, blendingmask, [custom user names]. - - Args: - stack_path (list or str, Optional): Path to the stack, could be - "Texture set name" or ["Texture set name", "Stack name"] - - Returns: - list: List of channel identifiers. - - """ - if stack_path is None: - stack_path = "" - else: - stack_path = _convert_stack_path_to_cmd_str(stack_path) - cmd = f"alg.mapexport.channelIdentifiers({stack_path})" - return substance_painter.js.evaluate(cmd) - - -def get_channel_format(stack_path, channel): - """Retrieve the channel format of a specific stack channel. - - See `alg.mapexport.channelFormat` (javascript API) for more details. - - The channel format data is: - "label" (str): The channel format label: could be one of - [sRGB8, L8, RGB8, L16, RGB16, L16F, RGB16F, L32F, RGB32F] - "color" (bool): True if the format is in color, False is grayscale - "floating" (bool): True if the format uses floating point - representation, false otherwise - "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) - - Arguments: - stack_path (list or str): Path to the stack, could be - "Texture set name" or ["Texture set name", "Stack name"] - channel (str): Identifier of the channel to export - (see `get_channel_identifiers`) - - Returns: - dict: The channel format data. - - """ - stack_path = _convert_stack_path_to_cmd_str(stack_path) - cmd = f"alg.mapexport.channelFormat({stack_path}, '{channel}')" - return substance_painter.js.evaluate(cmd) - - -def get_document_structure(): - """Dump the document structure. - - See `alg.mapexport.documentStructure` (javascript API) for more details. - - Returns: - dict: Document structure or None when no project is open - - """ - return substance_painter.js.evaluate("alg.mapexport.documentStructure()") - - -def get_export_templates(config, format="png", strip_folder=True): - """Return export config outputs. - - This use the Javascript API `alg.mapexport.getPathsExportDocumentMaps` - which returns a different output than using the Python equivalent - `substance_painter.export.list_project_textures(config)`. - - The nice thing about the Javascript API version is that it returns the - output textures grouped by filename template. - - A downside is that it doesn't return all the UDIM tiles but per template - always returns a single file. - - Note: - The file format needs to be explicitly passed to the Javascript API - but upon exporting through the Python API the file format can be based - on the output preset. So it's likely the file extension will mismatch - - Warning: - Even though the function appears to solely get the expected outputs - the Javascript API will actually create the config's texture output - folder if it does not exist yet. As such, a valid path must be set. - - Example output: - { - "DefaultMaterial": { - "$textureSet_BaseColor(_$colorSpace)(.$udim)": "DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", # noqa - "$textureSet_Emissive(_$colorSpace)(.$udim)": "DefaultMaterial_Emissive_ACES - ACEScg.1002.png", # noqa - "$textureSet_Height(_$colorSpace)(.$udim)": "DefaultMaterial_Height_Utility - Raw.1002.png", # noqa - "$textureSet_Metallic(_$colorSpace)(.$udim)": "DefaultMaterial_Metallic_Utility - Raw.1002.png", # noqa - "$textureSet_Normal(_$colorSpace)(.$udim)": "DefaultMaterial_Normal_Utility - Raw.1002.png", # noqa - "$textureSet_Roughness(_$colorSpace)(.$udim)": "DefaultMaterial_Roughness_Utility - Raw.1002.png" # noqa - } - } - - Arguments: - config (dict) Export config - format (str, Optional): Output format to write to, defaults to 'png' - strip_folder (bool, Optional): Whether to strip the output folder - from the output filenames. - - Returns: - dict: The expected output maps. - - """ - folder = config["exportPath"].replace("\\", "/") - preset = config["defaultExportPreset"] - cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa - result = substance_painter.js.evaluate(cmd) - - if strip_folder: - for _stack, maps in result.items(): - for map_template, map_filepath in maps.items(): - map_filepath = map_filepath.replace("\\", "/") - assert map_filepath.startswith(folder) - map_filename = map_filepath[len(folder):].lstrip("/") - maps[map_template] = map_filename - - return result - - -def _templates_to_regex(templates, - texture_set, - colorspaces, - project, - mesh): - """Return regex based on a Substance Painter expot filename template. - - This converts Substance Painter export filename templates like - `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` into a regex - which can be used to query an output filename to help retrieve: - - - Which template filename the file belongs to. - - Which color space the file is written with. - - Which udim tile it is exactly. - - This is used by `get_parsed_export_maps` which tries to as explicitly - as possible match the filename pattern against the known possible outputs. - That's why Texture Set name, Color spaces, Project path and mesh path must - be provided. By doing so we get the best shot at correctly matching the - right template because otherwise $texture_set could basically be any string - and thus match even that of a color space or mesh. - - Arguments: - templates (list): List of templates to convert to regex. - texture_set (str): The texture set to match against. - colorspaces (list): The colorspaces defined in the current project. - project (str): Filepath of current substance project. - mesh (str): Path to mesh file used in current project. - - Returns: - dict: Template: Template regex pattern - - """ - def _filename_no_ext(path): - return os.path.splitext(os.path.basename(path))[0] - - if colorspaces and any(colorspaces): - colorspace_match = "|".join(re.escape(c) for c in set(colorspaces)) - colorspace_match = f"({colorspace_match})" - else: - # No colorspace support enabled - colorspace_match = "" - - # Key to regex valid search values - key_matches = { - "$project": re.escape(_filename_no_ext(project)), - "$mesh": re.escape(_filename_no_ext(mesh)), - "$textureSet": re.escape(texture_set), - "$colorSpace": colorspace_match, - "$udim": "([0-9]{4})" - } - - # Turn the templates into regexes - regexes = {} - for template in templates: - - # We need to tweak a temp - search_regex = re.escape(template) - - # Let's assume that any ( and ) character in the file template was - # intended as an optional template key and do a simple `str.replace` - # Note: we are matching against re.escape(template) so will need to - # search for the escaped brackets. - search_regex = search_regex.replace(re.escape("("), "(") - search_regex = search_regex.replace(re.escape(")"), ")?") - - # Substitute each key into a named group - for key, key_expected_regex in key_matches.items(): - - # We want to use the template as a regex basis in the end so will - # escape the whole thing first. Note that thus we'll need to - # search for the escaped versions of the keys too. - escaped_key = re.escape(key) - key_label = key[1:] # key without $ prefix - - key_expected_grp_regex = f"(?P<{key_label}>{key_expected_regex})" - search_regex = search_regex.replace(escaped_key, - key_expected_grp_regex) - - # The filename templates don't include the extension so we add it - # to be able to match the out filename beginning to end - ext_regex = r"(?P\.[A-Za-z][A-Za-z0-9-]*)" - search_regex = rf"^{search_regex}{ext_regex}$" - - regexes[template] = search_regex - - return regexes - - -def strip_template(template, strip="._ "): - """Return static characters in a substance painter filename template. - - >>> strip_template("$textureSet_HELLO(.$udim)") - # HELLO - >>> strip_template("$mesh_$textureSet_HELLO_WORLD_$colorSpace(.$udim)") - # HELLO_WORLD - >>> strip_template("$textureSet_HELLO(.$udim)", strip=None) - # _HELLO - >>> strip_template("$mesh_$textureSet_$colorSpace(.$udim)", strip=None) - # _HELLO_ - >>> strip_template("$textureSet_HELLO(.$udim)") - # _HELLO - - Arguments: - template (str): Filename template to strip. - strip (str, optional): Characters to strip from beginning and end - of the static string in template. Defaults to: `._ `. - - Returns: - str: The static string in filename template. - - """ - # Return only characters that were part of the template that were static. - # Remove all keys - keys = ["$project", "$mesh", "$textureSet", "$udim", "$colorSpace"] - stripped_template = template - for key in keys: - stripped_template = stripped_template.replace(key, "") - - # Everything inside an optional bracket space is excluded since it's not - # static. We keep a counter to track whether we are currently iterating - # over parts of the template that are inside an 'optional' group or not. - counter = 0 - result = "" - for char in stripped_template: - if char == "(": - counter += 1 - elif char == ")": - counter -= 1 - if counter < 0: - counter = 0 - else: - if counter == 0: - result += char - - if strip: - # Strip of any trailing start/end characters. Technically these are - # static but usually start and end separators like space or underscore - # aren't wanted. - result = result.strip(strip) - - return result - - -def get_parsed_export_maps(config): - """Return Export Config's expected output textures with parsed data. - - This tries to parse the texture outputs using a Python API export config. - - Parses template keys: $project, $mesh, $textureSet, $colorSpace, $udim - - Example: - {("DefaultMaterial", ""): { - "$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)": [ - { - // OUTPUT DATA FOR FILE #1 OF THE TEMPLATE - }, - { - // OUTPUT DATA FOR FILE #2 OF THE TEMPLATE - }, - ] - }, - }} - - File output data (all outputs are `str`). - 1) Parsed tokens: These are parsed tokens from the template, they will - only exist if found in the filename template and output filename. - - project: Workfile filename without extension - mesh: Filename of the loaded mesh without extension - textureSet: The texture set, e.g. "DefaultMaterial", - colorSpace: The color space, e.g. "ACES - ACEScg", - udim: The udim tile, e.g. "1001" - - 2) Template output and filepath - - filepath: Full path to the resulting texture map, e.g. - "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", - output: "mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png" - Note: if template had slashes (folders) then `output` will too. - So `output` might include a folder. - - Returns: - dict: [texture_set, stack]: {template: [file1_data, file2_data]} - - """ - # Import is here to avoid recursive lib <-> colorspace imports - from .colorspace import get_project_channel_data - - outputs = substance_painter.export.list_project_textures(config) - templates = get_export_templates(config, strip_folder=False) - - # Get all color spaces set for the current project - project_colorspaces = set( - data["colorSpace"] for data in get_project_channel_data().values() - ) - - # Get current project mesh path and project path to explicitly match - # the $mesh and $project tokens - project_mesh_path = substance_painter.project.last_imported_mesh_path() - project_path = substance_painter.project.file_path() - - # Get the current export path to strip this of the beginning of filepath - # results, since filename templates don't have these we'll match without - # that part of the filename. - export_path = config["exportPath"] - export_path = export_path.replace("\\", "/") - if not export_path.endswith("/"): - export_path += "/" - - # Parse the outputs - result = {} - for key, filepaths in outputs.items(): - texture_set, stack = key - - if stack: - stack_path = f"{texture_set}/{stack}" - else: - stack_path = texture_set - - stack_templates = list(templates[stack_path].keys()) - - template_regex = _templates_to_regex(stack_templates, - texture_set=texture_set, - colorspaces=project_colorspaces, - mesh=project_mesh_path, - project=project_path) - - # Let's precompile the regexes - for template, regex in template_regex.items(): - template_regex[template] = re.compile(regex) - - stack_results = defaultdict(list) - for filepath in sorted(filepaths): - # We strip explicitly using the full parent export path instead of - # using `os.path.basename` because export template is allowed to - # have subfolders in its template which we want to match against - filepath = filepath.replace("\\", "/") - assert filepath.startswith(export_path), ( - f"Filepath {filepath} must start with folder {export_path}" - ) - filename = filepath[len(export_path):] - - for template, regex in template_regex.items(): - match = regex.match(filename) - if match: - parsed = match.groupdict(default={}) - - # Include some special outputs for convenience - parsed["filepath"] = filepath - parsed["output"] = filename - - stack_results[template].append(parsed) - break - else: - raise ValueError(f"Unable to match {filename} against any " - f"template in: {list(template_regex.keys())}") - - result[key] = dict(stack_results) - - return result - - -def load_shelf(path, name=None): - """Add shelf to substance painter (for current application session) - - This will dynamically add a Shelf for the current session. It's good - to note however that these will *not* persist on restart of the host. - - Note: - Consider the loaded shelf a static library of resources. - - The shelf will *not* be visible in application preferences in - Edit > Settings > Libraries. - - The shelf will *not* show in the Assets browser if it has no existing - assets - - The shelf will *not* be a selectable option for selecting it as a - destination to import resources too. - - """ - - # Ensure expanded path with forward slashes - path = os.path.expandvars(path) - path = os.path.abspath(path) - path = path.replace("\\", "/") - - # Path must exist - if not os.path.isdir(path): - raise ValueError(f"Path is not an existing folder: {path}") - - # This name must be unique and must only contain lowercase letters, - # numbers, underscores or hyphens. - if name is None: - name = os.path.basename(path) - - name = name.lower() - name = re.sub(r"[^a-z0-9_\-]", "_", name) # sanitize to underscores - - if substance_painter.resource.Shelves.exists(name): - shelf = next( - shelf for shelf in substance_painter.resource.Shelves.all() - if shelf.name() == name - ) - if os.path.normpath(shelf.path()) != os.path.normpath(path): - raise ValueError(f"Shelf with name '{name}' already exists " - f"for a different path: '{shelf.path()}") - - return - - print(f"Adding Shelf '{name}' to path: {path}") - substance_painter.resource.Shelves.add(name, path) - - return name - - -def _get_new_project_action(): - """Return QAction which triggers Substance Painter's new project dialog""" - - main_window = substance_painter.ui.get_main_window() - - # Find the file menu's New file action - menubar = main_window.menuBar() - new_action = None - for action in menubar.actions(): - menu = action.menu() - if not menu: - continue - - if menu.objectName() != "file": - continue - - # Find the action with the CTRL+N key sequence - new_action = next(action for action in menu.actions() - if action.shortcut() == QtGui.QKeySequence.New) - break - - return new_action - - -def prompt_new_file_with_mesh(mesh_filepath): - """Prompts the user for a new file using Substance Painter's own dialog. - - This will set the mesh path to load to the given mesh and disables the - dialog box to disallow the user to change the path. This way we can allow - user configuration of a project but set the mesh path ourselves. - - Warning: - This is very hacky and experimental. - - Note: - If a project is currently open using the same mesh filepath it can't - accurately detect whether the user had actually accepted the new project - dialog or whether the project afterwards is still the original project, - for example when the user might have cancelled the operation. - - """ - - app = QtWidgets.QApplication.instance() - assert os.path.isfile(mesh_filepath), \ - f"Mesh filepath does not exist: {mesh_filepath}" - - def _setup_file_dialog(): - """Set filepath in QFileDialog and trigger accept result""" - file_dialog = app.activeModalWidget() - assert isinstance(file_dialog, QtWidgets.QFileDialog) - - # Quickly hide the dialog - file_dialog.hide() - app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) - - file_dialog.setDirectory(os.path.dirname(mesh_filepath)) - url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) - file_dialog.selectUrl(url) - # TODO: find a way to improve the process event to - # load more complicated mesh - app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 3000) - file_dialog.done(file_dialog.Accepted) - app.processEvents(QtCore.QEventLoop.AllEvents) - - def _setup_prompt(): - app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) - dialog = app.activeModalWidget() - assert dialog.objectName() == "NewProjectDialog" - - # Set the window title - mesh = os.path.basename(mesh_filepath) - dialog.setWindowTitle(f"New Project with mesh: {mesh}") - - # Get the select mesh file button - mesh_select = dialog.findChild(QtWidgets.QPushButton, "meshSelect") - - # Hide the select mesh button to the user to block changing of mesh - mesh_select.setVisible(False) - - # Ensure UI is visually up-to-date - app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000) - - # Trigger the 'select file' dialog to set the path and have the - # new file dialog to use the path. - QtCore.QTimer.singleShot(10, _setup_file_dialog) - mesh_select.click() - - app.processEvents(QtCore.QEventLoop.AllEvents, 5000) - - mesh_filename = dialog.findChild(QtWidgets.QFrame, "meshFileName") - mesh_filename_label = mesh_filename.findChild(QtWidgets.QLabel) - if not mesh_filename_label.text(): - dialog.close() - substance_painter.logging.warning( - "Failed to set mesh path with the prompt dialog:" - f"{mesh_filepath}\n\n" - "Creating new project directly with the mesh path instead.") - - new_action = _get_new_project_action() - if not new_action: - raise RuntimeError("Unable to detect new file action..") - - QtCore.QTimer.singleShot(0, _setup_prompt) - new_action.trigger() - app.processEvents(QtCore.QEventLoop.AllEvents, 5000) - - if not substance_painter.project.is_open(): - return - - # Confirm mesh was set as expected - project_mesh = substance_painter.project.last_imported_mesh_path() - if os.path.normpath(project_mesh) != os.path.normpath(mesh_filepath): - return - - return project_mesh diff --git a/server_addon/substancepainter/client/substancepainter/api/pipeline.py b/server_addon/substancepainter/client/substancepainter/api/pipeline.py deleted file mode 100644 index 23d629533c..0000000000 --- a/server_addon/substancepainter/client/substancepainter/api/pipeline.py +++ /dev/null @@ -1,425 +0,0 @@ -# -*- coding: utf-8 -*- -"""Pipeline tools for OpenPype Substance Painter integration.""" -import os -import logging -from functools import partial - -# Substance 3D Painter modules -import substance_painter.ui -import substance_painter.event -import substance_painter.project - -import pyblish.api - -from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from ayon_core.settings import get_current_project_settings - -from ayon_core.pipeline.template_data import get_template_data_with_names -from ayon_core.pipeline import ( - register_creator_plugin_path, - register_loader_plugin_path, - AVALON_CONTAINER_ID, - Anatomy, -) -from ayon_core.lib import ( - StringTemplate, - register_event_callback, - emit_event, -) -from ayon_core.pipeline.load import any_outdated_containers -from ayon_core.hosts.substancepainter import SUBSTANCE_HOST_DIR - -from . import lib - -log = logging.getLogger("ayon_core.hosts.substance") - -PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - -OPENPYPE_METADATA_KEY = "OpenPype" -OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key -OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key -OPENPYPE_METADATA_INSTANCES_KEY = "instances" # child key - - -class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): - name = "substancepainter" - - def __init__(self): - super(SubstanceHost, self).__init__() - self._has_been_setup = False - self.menu = None - self.callbacks = [] - self.shelves = [] - - def install(self): - pyblish.api.register_host("substancepainter") - - pyblish.api.register_plugin_path(PUBLISH_PATH) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) - - log.info("Installing callbacks ... ") - # register_event_callback("init", on_init) - self._register_callbacks() - # register_event_callback("before.save", before_save) - # register_event_callback("save", on_save) - register_event_callback("open", on_open) - # register_event_callback("new", on_new) - - log.info("Installing menu ... ") - self._install_menu() - - project_settings = get_current_project_settings() - self._install_shelves(project_settings) - - self._has_been_setup = True - - def uninstall(self): - self._uninstall_shelves() - self._uninstall_menu() - self._deregister_callbacks() - - def workfile_has_unsaved_changes(self): - - if not substance_painter.project.is_open(): - return False - - return substance_painter.project.needs_saving() - - def get_workfile_extensions(self): - return [".spp", ".toc"] - - def save_workfile(self, dst_path=None): - - if not substance_painter.project.is_open(): - return False - - if not dst_path: - dst_path = self.get_current_workfile() - - full_save_mode = substance_painter.project.ProjectSaveMode.Full - substance_painter.project.save_as(dst_path, full_save_mode) - - return dst_path - - def open_workfile(self, filepath): - - if not os.path.exists(filepath): - raise RuntimeError("File does not exist: {}".format(filepath)) - - # We must first explicitly close current project before opening another - if substance_painter.project.is_open(): - substance_painter.project.close() - - substance_painter.project.open(filepath) - return filepath - - def get_current_workfile(self): - if not substance_painter.project.is_open(): - return None - - filepath = substance_painter.project.file_path() - if filepath and filepath.endswith(".spt"): - # When currently in a Substance Painter template assume our - # scene isn't saved. This can be the case directly after doing - # "New project", the path will then be the template used. This - # avoids Workfiles tool trying to save as .spt extension if the - # file hasn't been saved before. - return - - return filepath - - def get_containers(self): - - if not substance_painter.project.is_open(): - return - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) - if containers: - for key, container in containers.items(): - container["objectName"] = key - yield container - - def update_context_data(self, data, changes): - - if not substance_painter.project.is_open(): - return - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data) - - def get_context_data(self): - - if not substance_painter.project.is_open(): - return - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {} - - def _install_menu(self): - from PySide2 import QtWidgets - from ayon_core.tools.utils import host_tools - - parent = substance_painter.ui.get_main_window() - - tab_menu_label = os.environ.get("AYON_MENU_LABEL") or "AYON" - menu = QtWidgets.QMenu(tab_menu_label) - - action = menu.addAction("Create...") - action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent, - tab="create") - ) - - action = menu.addAction("Load...") - action.triggered.connect( - lambda: host_tools.show_loader(parent=parent, use_context=True) - ) - - action = menu.addAction("Publish...") - action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent, - tab="publish") - ) - - action = menu.addAction("Manage...") - action.triggered.connect( - lambda: host_tools.show_scene_inventory(parent=parent) - ) - - action = menu.addAction("Library...") - action.triggered.connect( - lambda: host_tools.show_library_loader(parent=parent) - ) - - menu.addSeparator() - action = menu.addAction("Work Files...") - action.triggered.connect( - lambda: host_tools.show_workfiles(parent=parent) - ) - - substance_painter.ui.add_menu(menu) - - def on_menu_destroyed(): - self.menu = None - - menu.destroyed.connect(on_menu_destroyed) - - self.menu = menu - - def _uninstall_menu(self): - if self.menu: - self.menu.destroy() - self.menu = None - - def _register_callbacks(self): - # Prepare emit event callbacks - open_callback = partial(emit_event, "open") - - # Connect to the Substance Painter events - dispatcher = substance_painter.event.DISPATCHER - for event, callback in [ - (substance_painter.event.ProjectOpened, open_callback) - ]: - dispatcher.connect(event, callback) - # Keep a reference so we can deregister if needed - self.callbacks.append((event, callback)) - - def _deregister_callbacks(self): - for event, callback in self.callbacks: - substance_painter.event.DISPATCHER.disconnect(event, callback) - self.callbacks.clear() - - def _install_shelves(self, project_settings): - - shelves = project_settings["substancepainter"].get("shelves", []) - if not shelves: - return - - # Prepare formatting data if we detect any path which might have - # template tokens like {folder[name]} in there. - formatting_data = {} - has_formatting_entries = any("{" in item["value"] for item in shelves) - if has_formatting_entries: - project_name = self.get_current_project_name() - folder_path = self.get_current_folder_path() - task_name = self.get_current_task_name() - formatting_data = get_template_data_with_names( - project_name, folder_path, task_name, project_settings - ) - anatomy = Anatomy(project_name) - formatting_data["root"] = anatomy.roots - - for shelve_item in shelves: - - # Allow formatting with anatomy for the paths - path = shelve_item["value"] - if "{" in path: - path = StringTemplate.format_template(path, formatting_data) - - name = shelve_item["name"] - shelf_name = None - try: - shelf_name = lib.load_shelf(path, name=name) - except ValueError as exc: - print(f"Failed to load shelf -> {exc}") - - if shelf_name: - self.shelves.append(shelf_name) - - def _uninstall_shelves(self): - for shelf_name in self.shelves: - substance_painter.resource.Shelves.remove(shelf_name) - self.shelves.clear() - - -def on_open(): - log.info("Running callback on open..") - - if any_outdated_containers(): - from ayon_core.tools.utils import SimplePopup - - log.warning("Scene has outdated content.") - - # Get main window - parent = substance_painter.ui.get_main_window() - if parent is None: - log.info("Skipping outdated content pop-up " - "because Substance window can't be found.") - else: - - # Show outdated pop-up - def _on_show_inventory(): - from ayon_core.tools.utils import host_tools - host_tools.show_scene_inventory(parent=parent) - - dialog = SimplePopup(parent=parent) - dialog.setWindowTitle("Substance scene has outdated content") - dialog.set_message("There are outdated containers in " - "your Substance scene.") - dialog.on_clicked.connect(_on_show_inventory) - dialog.show() - - -def imprint_container(container, - name, - namespace, - context, - loader): - """Imprint a loaded container with metadata. - - Containerisation enables a tracking of version, author and origin - for loaded assets. - - Arguments: - container (dict): The (substance metadata) dictionary to imprint into. - name (str): Name of resulting assembly - namespace (str): Namespace under which to host container - context (dict): Asset information - loader (load.LoaderPlugin): loader instance used to produce container. - - Returns: - None - - """ - - data = [ - ("schema", "openpype:container-2.0"), - ("id", AVALON_CONTAINER_ID), - ("name", str(name)), - ("namespace", str(namespace) if namespace else None), - ("loader", str(loader.__class__.__name__)), - ("representation", context["representation"]["id"]), - ] - for key, value in data: - container[key] = value - - -def set_container_metadata(object_name, container_data, update=False): - """Helper method to directly set the data for a specific container - - Args: - object_name (str): The unique object name identifier for the container - container_data (dict): The data for the container. - Note 'objectName' data is derived from `object_name` and key in - `container_data` will be ignored. - update (bool): Whether to only update the dict data. - - """ - # The objectName is derived from the key in the metadata so won't be stored - # in the metadata in the container's data. - container_data.pop("objectName", None) - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {} - if update: - existing_data = containers.setdefault(object_name, {}) - existing_data.update(container_data) # mutable dict, in-place update - else: - containers[object_name] = container_data - metadata.set("containers", containers) - - -def remove_container_metadata(object_name): - """Helper method to remove the data for a specific container""" - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) - if containers: - containers.pop(object_name, None) - metadata.set("containers", containers) - - -def set_instance(instance_id, instance_data, update=False): - """Helper method to directly set the data for a specific container - - Args: - instance_id (str): Unique identifier for the instance - instance_data (dict): The instance data to store in the metaadata. - """ - set_instances({instance_id: instance_data}, update=update) - - -def set_instances(instance_data_by_id, update=False): - """Store data for multiple instances at the same time. - - This is more optimal than querying and setting them in the metadata one - by one. - """ - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} - - for instance_id, instance_data in instance_data_by_id.items(): - if update: - existing_data = instances.get(instance_id, {}) - existing_data.update(instance_data) - else: - instances[instance_id] = instance_data - - metadata.set("instances", instances) - - -def remove_instance(instance_id): - """Helper method to remove the data for a specific container""" - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} - instances.pop(instance_id, None) - metadata.set("instances", instances) - - -def get_instances_by_id(): - """Return all instances stored in the project instances metadata""" - if not substance_painter.project.is_open(): - return {} - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} - - -def get_instances(): - """Return all instances stored in the project instances as a list""" - return list(get_instances_by_id().values()) diff --git a/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py b/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py deleted file mode 100644 index 8ced463367..0000000000 --- a/server_addon/substancepainter/client/substancepainter/deploy/plugins/openpype_plugin.py +++ /dev/null @@ -1,36 +0,0 @@ - - -def cleanup_openpype_qt_widgets(): - """ - Workaround for Substance failing to shut down correctly - when a Qt window was still open at the time of shutting down. - - This seems to work sometimes, but not all the time. - - """ - # TODO: Create a more reliable method to close down all OpenPype Qt widgets - from PySide2 import QtWidgets - import substance_painter.ui - - # Kill OpenPype Qt widgets - print("Killing OpenPype Qt widgets..") - for widget in QtWidgets.QApplication.topLevelWidgets(): - if widget.__module__.startswith("openpype."): - print(f"Deleting widget: {widget.__class__.__name__}") - substance_painter.ui.delete_ui_element(widget) - - -def start_plugin(): - from ayon_core.pipeline import install_host - from ayon_core.hosts.substancepainter.api import SubstanceHost - install_host(SubstanceHost()) - - -def close_plugin(): - from ayon_core.pipeline import uninstall_host - cleanup_openpype_qt_widgets() - uninstall_host() - - -if __name__ == "__main__": - start_plugin() diff --git a/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py b/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py deleted file mode 100644 index 04b610b4df..0000000000 --- a/server_addon/substancepainter/client/substancepainter/deploy/startup/openpype_load_on_first_run.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Ease the OpenPype on-boarding process by loading the plug-in on first run""" - -OPENPYPE_PLUGIN_NAME = "openpype_plugin" - - -def start_plugin(): - try: - # This isn't exposed in the official API so we keep it in a try-except - from painter_plugins_ui import ( - get_settings, - LAUNCH_AT_START_KEY, - ON_STATE, - PLUGINS_MENU, - plugin_manager - ) - - # The `painter_plugins_ui` plug-in itself is also a startup plug-in - # we need to take into account that it could run either earlier or - # later than this startup script, we check whether its menu initialized - is_before_plugins_menu = PLUGINS_MENU is None - - settings = get_settings(OPENPYPE_PLUGIN_NAME) - if settings.value(LAUNCH_AT_START_KEY, None) is None: - print("Initializing OpenPype plug-in on first run...") - if is_before_plugins_menu: - print("- running before 'painter_plugins_ui'") - # Delay the launch to the painter_plugins_ui initialization - settings.setValue(LAUNCH_AT_START_KEY, ON_STATE) - else: - # Launch now - print("- running after 'painter_plugins_ui'") - plugin_manager(OPENPYPE_PLUGIN_NAME)(True) - - # Set the checked state in the menu to avoid confusion - action = next(action for action in PLUGINS_MENU._menu.actions() - if action.text() == OPENPYPE_PLUGIN_NAME) - if action is not None: - action.blockSignals(True) - action.setChecked(True) - action.blockSignals(False) - - except Exception as exc: - print(exc) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py b/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py deleted file mode 100644 index f46afadb5a..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/create/create_textures.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator plugin for creating textures.""" - -from ayon_core.pipeline import CreatedInstance, Creator, CreatorError -from ayon_core.lib import ( - EnumDef, - UILabelDef, - NumberDef, - BoolDef -) - -from ayon_core.hosts.substancepainter.api.pipeline import ( - get_instances, - set_instance, - set_instances, - remove_instance -) -from ayon_core.hosts.substancepainter.api.lib import get_export_presets - -import substance_painter.project - - -class CreateTextures(Creator): - """Create a texture set.""" - identifier = "io.openpype.creators.substancepainter.textureset" - label = "Textures" - product_type = "textureSet" - icon = "picture-o" - - default_variant = "Main" - - def create(self, product_name, instance_data, pre_create_data): - - if not substance_painter.project.is_open(): - raise CreatorError("Can't create a Texture Set instance without " - "an open project.") - # Transfer settings from pre create to instance - creator_attributes = instance_data.setdefault( - "creator_attributes", dict()) - for key in [ - "exportPresetUrl", - "exportFileFormat", - "exportSize", - "exportPadding", - "exportDilationDistance" - ]: - if key in pre_create_data: - creator_attributes[key] = pre_create_data[key] - - instance = self.create_instance_in_context(product_name, - instance_data) - set_instance( - instance_id=instance["instance_id"], - instance_data=instance.data_to_store() - ) - - def collect_instances(self): - for instance in get_instances(): - if (instance.get("creator_identifier") == self.identifier or - instance.get("productType") == self.product_type): - self.create_instance_in_context_from_existing(instance) - - def update_instances(self, update_list): - instance_data_by_id = {} - for instance, _changes in update_list: - # Persist the data - instance_id = instance.get("instance_id") - instance_data = instance.data_to_store() - instance_data_by_id[instance_id] = instance_data - set_instances(instance_data_by_id, update=True) - - def remove_instances(self, instances): - for instance in instances: - remove_instance(instance["instance_id"]) - self._remove_instance_from_context(instance) - - # Helper methods (this might get moved into Creator class) - def create_instance_in_context(self, product_name, data): - instance = CreatedInstance( - self.product_type, product_name, data, self - ) - self.create_context.creator_adds_instance(instance) - return instance - - def create_instance_in_context_from_existing(self, data): - instance = CreatedInstance.from_existing(data, self) - self.create_context.creator_adds_instance(instance) - return instance - - def get_instance_attr_defs(self): - - return [ - EnumDef("exportPresetUrl", - items=get_export_presets(), - label="Output Template"), - BoolDef("allowSkippedMaps", - label="Allow Skipped Output Maps", - tooltip="When enabled this allows the publish to ignore " - "output maps in the used output template if one " - "or more maps are skipped due to the required " - "channels not being present in the current file.", - default=True), - EnumDef("exportFileFormat", - items={ - None: "Based on output template", - # TODO: Get available extensions from substance API - "bmp": "bmp", - "ico": "ico", - "jpeg": "jpeg", - "jng": "jng", - "pbm": "pbm", - "pgm": "pgm", - "png": "png", - "ppm": "ppm", - "tga": "targa", - "tif": "tiff", - "wap": "wap", - "wbmp": "wbmp", - "xpm": "xpm", - "gif": "gif", - "hdr": "hdr", - "exr": "exr", - "j2k": "j2k", - "jp2": "jp2", - "pfm": "pfm", - "webp": "webp", - # TODO: Unsure why jxr format fails to export - # "jxr": "jpeg-xr", - # TODO: File formats that combine the exported textures - # like psd are not correctly supported due to - # publishing only a single file - # "psd": "psd", - # "sbsar": "sbsar", - }, - default=None, - label="File type"), - EnumDef("exportSize", - items={ - None: "Based on each Texture Set's size", - # The key is size of the texture file in log2. - # (i.e. 10 means 2^10 = 1024) - 7: "128", - 8: "256", - 9: "512", - 10: "1024", - 11: "2048", - 12: "4096", - 13: "8192" - }, - default=None, - label="Size"), - - EnumDef("exportPadding", - items={ - "passthrough": "No padding (passthrough)", - "infinite": "Dilation infinite", - "transparent": "Dilation + transparent", - "color": "Dilation + default background color", - "diffusion": "Dilation + diffusion" - }, - default="infinite", - label="Padding"), - NumberDef("exportDilationDistance", - minimum=0, - maximum=256, - decimals=0, - default=16, - label="Dilation Distance"), - UILabelDef("*only used with " - "'Dilation + ' padding"), - ] - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return self.get_instance_attr_defs() diff --git a/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py deleted file mode 100644 index 63b1c6c7da..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/create/create_workfile.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator plugin for creating workfiles.""" - -import ayon_api - -from ayon_core.pipeline import CreatedInstance, AutoCreator - -from ayon_core.hosts.substancepainter.api.pipeline import ( - set_instances, - set_instance, - get_instances -) - -import substance_painter.project - - -class CreateWorkfile(AutoCreator): - """Workfile auto-creator.""" - identifier = "io.openpype.creators.substancepainter.workfile" - label = "Workfile" - product_type = "workfile" - icon = "document" - - default_variant = "Main" - - def create(self): - - if not substance_painter.project.is_open(): - return - - variant = self.default_variant - project_name = self.project_name - folder_path = self.create_context.get_current_folder_path() - task_name = self.create_context.get_current_task_name() - host_name = self.create_context.host_name - - # Workfile instance should always exist and must only exist once. - # As such we'll first check if it already exists and is collected. - current_instance = next( - ( - instance for instance in self.create_context.instances - if instance.creator_identifier == self.identifier - ), None) - - current_folder_path = None - if current_instance is not None: - current_folder_path = current_instance["folderPath"] - - if current_instance is None: - self.log.info("Auto-creating workfile instance...") - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path - ) - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name - ) - product_name = self.get_product_name( - project_name, - folder_entity, - task_entity, - variant, - host_name, - ) - data = { - "folderPath": folder_path, - "task": task_name, - "variant": variant - } - current_instance = self.create_instance_in_context(product_name, - data) - elif ( - current_folder_path != folder_path - or current_instance["task"] != task_name - ): - # Update instance context if is not the same - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path - ) - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name - ) - product_name = self.get_product_name( - project_name, - folder_entity, - task_entity, - variant, - host_name, - ) - current_instance["folderPath"] = folder_path - current_instance["task"] = task_name - current_instance["productName"] = product_name - - set_instance( - instance_id=current_instance.get("instance_id"), - instance_data=current_instance.data_to_store() - ) - - def collect_instances(self): - for instance in get_instances(): - if (instance.get("creator_identifier") == self.identifier or - instance.get("productType") == self.product_type): - self.create_instance_in_context_from_existing(instance) - - def update_instances(self, update_list): - instance_data_by_id = {} - for instance, _changes in update_list: - # Persist the data - instance_id = instance.get("instance_id") - instance_data = instance.data_to_store() - instance_data_by_id[instance_id] = instance_data - set_instances(instance_data_by_id, update=True) - - # Helper methods (this might get moved into Creator class) - def create_instance_in_context(self, product_name, data): - instance = CreatedInstance( - self.product_type, product_name, data, self - ) - self.create_context.creator_adds_instance(instance) - return instance - - def create_instance_in_context_from_existing(self, data): - instance = CreatedInstance.from_existing(data, self) - self.create_context.creator_adds_instance(instance) - return instance diff --git a/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py b/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py deleted file mode 100644 index d5aac1191c..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/load/load_mesh.py +++ /dev/null @@ -1,246 +0,0 @@ -import copy -from qtpy import QtWidgets, QtCore -from ayon_core.pipeline import ( - load, - get_representation_path, -) -from ayon_core.pipeline.load import LoadError -from ayon_core.hosts.substancepainter.api.pipeline import ( - imprint_container, - set_container_metadata, - remove_container_metadata -) - -import substance_painter.project - - -def _convert(substance_attr): - """Return Substance Painter Python API Project attribute from string. - - This converts a string like "ProjectWorkflow.Default" to for example - the Substance Painter Python API equivalent object, like: - `substance_painter.project.ProjectWorkflow.Default` - - Args: - substance_attr (str): The `substance_painter.project` attribute, - for example "ProjectWorkflow.Default" - - Returns: - Any: Substance Python API object of the project attribute. - - Raises: - ValueError: If attribute does not exist on the - `substance_painter.project` python api. - """ - root = substance_painter.project - for attr in substance_attr.split("."): - root = getattr(root, attr, None) - if root is None: - raise ValueError( - "Substance Painter project attribute" - f" does not exist: {substance_attr}") - - return root - - -def get_template_by_name(name: str, templates: list[dict]) -> dict: - return next( - template for template in templates - if template["name"] == name - ) - - -class SubstanceProjectConfigurationWindow(QtWidgets.QDialog): - """The pop-up dialog allows users to choose material - duplicate options for importing Max objects when updating - or switching assets. - """ - def __init__(self, project_templates): - super(SubstanceProjectConfigurationWindow, self).__init__() - self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) - - self.configuration = None - self.template_names = [template["name"] for template - in project_templates] - self.project_templates = project_templates - - self.widgets = { - "label": QtWidgets.QLabel( - "Select your template for project configuration"), - "template_options": QtWidgets.QComboBox(), - "import_cameras": QtWidgets.QCheckBox("Import Cameras"), - "preserve_strokes": QtWidgets.QCheckBox("Preserve Strokes"), - "clickbox": QtWidgets.QWidget(), - "combobox": QtWidgets.QWidget(), - "buttons": QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok - | QtWidgets.QDialogButtonBox.Cancel) - } - - self.widgets["template_options"].addItems(self.template_names) - - template_name = self.widgets["template_options"].currentText() - self._update_to_match_template(template_name) - # Build clickboxes - layout = QtWidgets.QHBoxLayout(self.widgets["clickbox"]) - layout.addWidget(self.widgets["import_cameras"]) - layout.addWidget(self.widgets["preserve_strokes"]) - # Build combobox - layout = QtWidgets.QHBoxLayout(self.widgets["combobox"]) - layout.addWidget(self.widgets["template_options"]) - # Build buttons - layout = QtWidgets.QHBoxLayout(self.widgets["buttons"]) - # Build layout. - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(self.widgets["label"]) - layout.addWidget(self.widgets["combobox"]) - layout.addWidget(self.widgets["clickbox"]) - layout.addWidget(self.widgets["buttons"]) - - self.widgets["template_options"].currentTextChanged.connect( - self._update_to_match_template) - self.widgets["buttons"].accepted.connect(self.on_accept) - self.widgets["buttons"].rejected.connect(self.on_reject) - - def on_accept(self): - self.configuration = self.get_project_configuration() - self.close() - - def on_reject(self): - self.close() - - def _update_to_match_template(self, template_name): - template = get_template_by_name(template_name, self.project_templates) - self.widgets["import_cameras"].setChecked(template["import_cameras"]) - self.widgets["preserve_strokes"].setChecked( - template["preserve_strokes"]) - - def get_project_configuration(self): - templates = self.project_templates - template_name = self.widgets["template_options"].currentText() - template = get_template_by_name(template_name, templates) - template = copy.deepcopy(template) # do not edit the original - template["import_cameras"] = self.widgets["import_cameras"].isChecked() - template["preserve_strokes"] = ( - self.widgets["preserve_strokes"].isChecked() - ) - for key in ["normal_map_format", - "project_workflow", - "tangent_space_mode"]: - template[key] = _convert(template[key]) - return template - - @classmethod - def prompt(cls, templates): - dialog = cls(templates) - dialog.exec_() - configuration = dialog.configuration - dialog.deleteLater() - return configuration - - -class SubstanceLoadProjectMesh(load.LoaderPlugin): - """Load mesh for project""" - - product_types = {"*"} - representations = {"abc", "fbx", "obj", "gltf", "usd", "usda", "usdc"} - - label = "Load mesh" - order = -10 - icon = "code-fork" - color = "orange" - - # Defined via settings - project_templates = [] - - def load(self, context, name, namespace, options=None): - - # Get user inputs - result = SubstanceProjectConfigurationWindow.prompt( - self.project_templates) - if not result: - # cancelling loader action - return - if not substance_painter.project.is_open(): - # Allow to 'initialize' a new project - path = self.filepath_from_context(context) - sp_settings = substance_painter.project.Settings( - import_cameras=result["import_cameras"], - normal_map_format=result["normal_map_format"], - project_workflow=result["project_workflow"], - tangent_space_mode=result["tangent_space_mode"], - default_texture_resolution=result["default_texture_resolution"] - ) - settings = substance_painter.project.create( - mesh_file_path=path, settings=sp_settings - ) - else: - # Reload the mesh - settings = substance_painter.project.MeshReloadingSettings( - import_cameras=result["import_cameras"], - preserve_strokes=result["preserve_strokes"]) - - def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa - if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa - self.log.info("Reload succeeded") - else: - raise LoadError("Reload of mesh failed") - - path = self.filepath_from_context(context) - substance_painter.project.reload_mesh(path, - settings, - on_mesh_reload) - - # Store container - container = {} - project_mesh_object_name = "_ProjectMesh_" - imprint_container(container, - name=project_mesh_object_name, - namespace=project_mesh_object_name, - context=context, - loader=self) - - # We want store some options for updating to keep consistent behavior - # from the user's original choice. We don't store 'preserve_strokes' - # as we always preserve strokes on updates. - container["options"] = { - "import_cameras": result["import_cameras"], - } - - set_container_metadata(project_mesh_object_name, container) - - def switch(self, container, context): - self.update(container, context) - - def update(self, container, context): - repre_entity = context["representation"] - - path = get_representation_path(repre_entity) - - # Reload the mesh - container_options = container.get("options", {}) - settings = substance_painter.project.MeshReloadingSettings( - import_cameras=container_options.get("import_cameras", True), - preserve_strokes=True - ) - - def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): - if status == substance_painter.project.ReloadMeshStatus.SUCCESS: - self.log.info("Reload succeeded") - else: - raise LoadError("Reload of mesh failed") - - substance_painter.project.reload_mesh(path, settings, on_mesh_reload) - - # Update container representation - object_name = container["objectName"] - update_data = {"representation": repre_entity["id"]} - set_container_metadata(object_name, update_data, update=True) - - def remove(self, container): - - # Remove OpenPype related settings about what model was loaded - # or close the project? - # TODO: This is likely best 'hidden' away to the user because - # this will leave the project's mesh unmanaged. - remove_container_metadata(container["objectName"]) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py deleted file mode 100644 index db0edafac0..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_current_file.py +++ /dev/null @@ -1,17 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import registered_host - - -class CollectCurrentFile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - order = pyblish.api.CollectorOrder - 0.49 - label = "Current Workfile" - hosts = ["substancepainter"] - - def process(self, context): - host = registered_host() - path = host.get_current_workfile() - context.data["currentFile"] = path - self.log.debug(f"Current workfile: {path}") diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py deleted file mode 100644 index 20aaa56993..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_textureset_images.py +++ /dev/null @@ -1,211 +0,0 @@ -import os -import copy - -import pyblish.api -import ayon_api - -import substance_painter.textureset -from ayon_core.pipeline import publish -from ayon_core.hosts.substancepainter.api.lib import ( - get_parsed_export_maps, - strip_template -) -from ayon_core.pipeline.create import get_product_name - - -class CollectTextureSet(pyblish.api.InstancePlugin): - """Extract Textures using an output template config""" - # TODO: Production-test usage of color spaces - # TODO: Detect what source data channels end up in each file - - label = "Collect Texture Set images" - hosts = ["substancepainter"] - families = ["textureSet"] - order = pyblish.api.CollectorOrder - - def process(self, instance): - - config = self.get_export_config(instance) - project_name = instance.context.data["projectName"] - folder_entity = ayon_api.get_folder_by_path( - project_name, - instance.data["folderPath"] - ) - task_name = instance.data.get("task") - task_entity = None - if folder_entity and task_name: - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name - ) - - instance.data["exportConfig"] = config - maps = get_parsed_export_maps(config) - - # Let's break the instance into multiple instances to integrate - # a product per generated texture or texture UDIM sequence - for (texture_set_name, stack_name), template_maps in maps.items(): - self.log.info(f"Processing {texture_set_name}/{stack_name}") - for template, outputs in template_maps.items(): - self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs, - task_entity=task_entity, - texture_set_name=texture_set_name, - stack_name=stack_name) - - def create_image_instance(self, instance, template, outputs, - task_entity, texture_set_name, stack_name): - """Create a new instance per image or UDIM sequence. - - The new instances will be of product type `image`. - - """ - - context = instance.context - first_filepath = outputs[0]["filepath"] - fnames = [os.path.basename(output["filepath"]) for output in outputs] - ext = os.path.splitext(first_filepath)[1] - assert ext.lstrip("."), f"No extension: {ext}" - - always_include_texture_set_name = False # todo: make this configurable - all_texture_sets = substance_painter.textureset.all_texture_sets() - texture_set = substance_painter.textureset.TextureSet.from_name( - texture_set_name - ) - - # Define the suffix we want to give this particular texture - # set and set up a remapped product naming for it. - suffix = "" - if always_include_texture_set_name or len(all_texture_sets) > 1: - # More than one texture set, include texture set name - suffix += f".{texture_set_name}" - if texture_set.is_layered_material() and stack_name: - # More than one stack, include stack name - suffix += f".{stack_name}" - - # Always include the map identifier - map_identifier = strip_template(template) - suffix += f".{map_identifier}" - - task_name = task_type = None - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] - - image_product_name = get_product_name( - # TODO: The product type actually isn't 'texture' currently but - # for now this is only done so the product name starts with - # 'texture' - context.data["projectName"], - task_name, - task_type, - context.data["hostName"], - product_type="texture", - variant=instance.data["variant"] + suffix, - project_settings=context.data["project_settings"] - ) - - # Prepare representation - representation = { - "name": ext.lstrip("."), - "ext": ext.lstrip("."), - "files": fnames if len(fnames) > 1 else fnames[0], - } - - # Mark as UDIM explicitly if it has UDIM tiles. - if bool(outputs[0].get("udim")): - # The representation for a UDIM sequence should have a `udim` key - # that is a list of all udim tiles (str) like: ["1001", "1002"] - # strings. See CollectTextures plug-in and Integrators. - representation["udim"] = [output["udim"] for output in outputs] - - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_filepath) - representation["tags"] = ["review"] - representation["stagingDir"] = staging_dir - - # Clone the instance - product_type = "image" - image_instance = context.create_instance(image_product_name) - image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(dict(instance.data))) - image_instance.data["name"] = image_product_name - image_instance.data["label"] = image_product_name - image_instance.data["productName"] = image_product_name - image_instance.data["productType"] = product_type - image_instance.data["family"] = product_type - image_instance.data["families"] = [product_type, "textures"] - image_instance.data["representations"] = [representation] - - # Group the textures together in the loader - image_instance.data["productGroup"] = image_product_name - - # Store the texture set name and stack name on the instance - image_instance.data["textureSetName"] = texture_set_name - image_instance.data["textureStackName"] = stack_name - - # Store color space with the instance - # Note: The extractor will assign it to the representation - colorspace = outputs[0].get("colorSpace") - if colorspace: - self.log.debug(f"{image_product_name} colorspace: {colorspace}") - image_instance.data["colorspace"] = colorspace - - # Store the instance in the original instance as a member - instance.append(image_instance) - - def get_export_config(self, instance): - """Return an export configuration dict for texture exports. - - This config can be supplied to: - - `substance_painter.export.export_project_textures` - - `substance_painter.export.list_project_textures` - - See documentation on substance_painter.export module about the - formatting of the configuration dictionary. - - Args: - instance (pyblish.api.Instance): Texture Set instance to be - published. - - Returns: - dict: Export config - - """ - - creator_attrs = instance.data["creator_attributes"] - preset_url = creator_attrs["exportPresetUrl"] - self.log.debug(f"Exporting using preset: {preset_url}") - - # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa - config = { # noqa - "exportShaderParams": True, - "exportPath": publish.get_instance_staging_dir(instance), - "defaultExportPreset": preset_url, - - # Custom overrides to the exporter - "exportParameters": [ - { - "parameters": { - "fileFormat": creator_attrs["exportFileFormat"], - "sizeLog2": creator_attrs["exportSize"], - "paddingAlgorithm": creator_attrs["exportPadding"], - "dilationDistance": creator_attrs["exportDilationDistance"] # noqa - } - } - ] - } - - # Create the list of Texture Sets to export. - config["exportList"] = [] - for texture_set in substance_painter.textureset.all_texture_sets(): - config["exportList"].append({"rootPath": texture_set.name()}) - - # Consider None values from the creator attributes optionals - for override in config["exportParameters"]: - parameters = override.get("parameters") - for key, value in dict(parameters).items(): - if value is None: - parameters.pop(key) - - return config diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py deleted file mode 100644 index 8d98d0b014..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/collect_workfile_representation.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import pyblish.api - - -class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): - """Create a publish representation for the current workfile instance.""" - - order = pyblish.api.CollectorOrder - label = "Workfile representation" - hosts = ["substancepainter"] - families = ["workfile"] - - def process(self, instance): - - context = instance.context - current_file = context.data["currentFile"] - - folder, file = os.path.split(current_file) - filename, ext = os.path.splitext(file) - - instance.data["representations"] = [{ - "name": ext.lstrip("."), - "ext": ext.lstrip("."), - "files": file, - "stagingDir": folder, - }] diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py deleted file mode 100644 index 0fa7b52f45..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/extract_textures.py +++ /dev/null @@ -1,62 +0,0 @@ -import substance_painter.export - -from ayon_core.pipeline import KnownPublishError, publish - - -class ExtractTextures(publish.Extractor, - publish.ColormanagedPyblishPluginMixin): - """Extract Textures using an output template config. - - Note: - This Extractor assumes that `collect_textureset_images` has prepared - the relevant export config and has also collected the individual image - instances for publishing including its representation. That is why this - particular Extractor doesn't specify representations to integrate. - - """ - - label = "Extract Texture Set" - hosts = ["substancepainter"] - families = ["textureSet"] - - # Run before thumbnail extractors - order = publish.Extractor.order - 0.1 - - def process(self, instance): - - config = instance.data["exportConfig"] - result = substance_painter.export.export_project_textures(config) - - if result.status != substance_painter.export.ExportStatus.Success: - raise KnownPublishError( - "Failed to export texture set: {}".format(result.message) - ) - - # Log what files we generated - for (texture_set_name, stack_name), maps in result.textures.items(): - # Log our texture outputs - self.log.info(f"Exported stack: {texture_set_name} {stack_name}") - for texture_map in maps: - self.log.info(f"Exported texture: {texture_map}") - - # We'll insert the color space data for each image instance that we - # added into this texture set. The collector couldn't do so because - # some anatomy and other instance data needs to be collected prior - context = instance.context - for image_instance in instance: - representation = next(iter(image_instance.data["representations"])) - - colorspace = image_instance.data.get("colorspace") - if not colorspace: - self.log.debug("No color space data present for instance: " - f"{image_instance}") - continue - - self.set_representation_colorspace(representation, - context=context, - colorspace=colorspace) - - # The TextureSet instance should not be integrated. It generates no - # output data. Instead the separated texture instances are generated - # from it which themselves integrate into the database. - instance.data["integrate"] = False diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py deleted file mode 100644 index 521a28130b..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/increment_workfile.py +++ /dev/null @@ -1,23 +0,0 @@ -import pyblish.api - -from ayon_core.lib import version_up -from ayon_core.pipeline import registered_host - - -class IncrementWorkfileVersion(pyblish.api.ContextPlugin): - """Increment current workfile version.""" - - order = pyblish.api.IntegratorOrder + 1 - label = "Increment Workfile Version" - optional = True - hosts = ["substancepainter"] - - def process(self, context): - - assert all(result["success"] for result in context.data["results"]), ( - "Publishing not successful so version is not increased.") - - host = registered_host() - path = context.data["currentFile"] - self.log.info(f"Incrementing current workfile to: {path}") - host.save_workfile(version_up(path)) diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py deleted file mode 100644 index 627fb991aa..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/save_workfile.py +++ /dev/null @@ -1,28 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import ( - registered_host, - KnownPublishError -) - - -class SaveCurrentWorkfile(pyblish.api.ContextPlugin): - """Save current workfile""" - - label = "Save current workfile" - order = pyblish.api.ExtractorOrder - 0.49 - hosts = ["substancepainter"] - - def process(self, context): - - host = registered_host() - current = host.get_current_workfile() - if context.data["currentFile"] != current: - raise KnownPublishError("Workfile has changed during publishing!") - - if host.workfile_has_unsaved_changes(): - self.log.info("Saving current file: {}".format(current)) - host.save_workfile() - else: - self.log.debug("Skipping workfile save because there are no " - "unsaved changes.") diff --git a/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py b/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py deleted file mode 100644 index 720771994c..0000000000 --- a/server_addon/substancepainter/client/substancepainter/plugins/publish/validate_ouput_maps.py +++ /dev/null @@ -1,110 +0,0 @@ -import copy -import os - -import pyblish.api - -import substance_painter.export - -from ayon_core.pipeline import PublishValidationError - - -class ValidateOutputMaps(pyblish.api.InstancePlugin): - """Validate all output maps for Output Template are generated. - - Output maps will be skipped by Substance Painter if it is an output - map in the Substance Output Template which uses channels that the current - substance painter project has not painted or generated. - - """ - - order = pyblish.api.ValidatorOrder - label = "Validate output maps" - hosts = ["substancepainter"] - families = ["textureSet"] - - def process(self, instance): - - config = instance.data["exportConfig"] - - # Substance Painter API does not allow to query the actual output maps - # it will generate without actually exporting the files. So we try to - # generate the smallest size / fastest export as possible - config = copy.deepcopy(config) - parameters = config["exportParameters"][0]["parameters"] - parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) - parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) - parameters["dithering"] = False # no dithering (faster) - - result = substance_painter.export.export_project_textures(config) - if result.status != substance_painter.export.ExportStatus.Success: - raise PublishValidationError( - "Failed to export texture set: {}".format(result.message) - ) - - generated_files = set() - for texture_maps in result.textures.values(): - for texture_map in texture_maps: - generated_files.add(os.path.normpath(texture_map)) - # Directly clean up our temporary export - os.remove(texture_map) - - creator_attributes = instance.data.get("creator_attributes", {}) - allow_skipped_maps = creator_attributes.get("allowSkippedMaps", True) - error_report_missing = [] - for image_instance in instance: - - # Confirm whether the instance has its expected files generated. - # We assume there's just one representation and that it is - # the actual texture representation from the collector. - representation = next(iter(image_instance.data["representations"])) - staging_dir = representation["stagingDir"] - filenames = representation["files"] - if not isinstance(filenames, (list, tuple)): - # Convert single file to list - filenames = [filenames] - - missing = [] - for filename in filenames: - filepath = os.path.join(staging_dir, filename) - filepath = os.path.normpath(filepath) - if filepath not in generated_files: - self.log.warning(f"Missing texture: {filepath}") - missing.append(filepath) - - if not missing: - continue - - if allow_skipped_maps: - # TODO: This is changing state on the instance's which - # should not be done during validation. - self.log.warning(f"Disabling texture instance: " - f"{image_instance}") - image_instance.data["active"] = False - image_instance.data["publish"] = False - image_instance.data["integrate"] = False - representation.setdefault("tags", []).append("delete") - continue - else: - error_report_missing.append((image_instance, missing)) - - if error_report_missing: - - message = ( - "The Texture Set skipped exporting some output maps which are " - "defined in the Output Template. This happens if the Output " - "Templates exports maps from channels which you do not " - "have in your current Substance Painter project.\n\n" - "To allow this enable the *Allow Skipped Output Maps* setting " - "on the instance.\n\n" - f"Instance {instance} skipped exporting output maps:\n" - "" - ) - - for image_instance, missing in error_report_missing: - missing_str = ", ".join(missing) - message += f"- **{image_instance}** skipped: {missing_str}\n" - - raise PublishValidationError( - message=message, - title="Missing output maps" - ) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index d445b0059f..66638b0a2e 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -1,3 +1,10 @@ name = "substancepainter" title = "Substance Painter" -version = "0.1.1" +version = "0.1.2" + +client_dir = "ayon_substancepainter" + +ayon_required_addons = { + "core": ">=0.3.1", +} +ayon_compatible_addons = {} From bad4de2f7c0f1143648d8c90ac18f9ddef0617a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 17:06:53 +0800 Subject: [PATCH 06/65] move substance painter integration to server-addon and rename the folder as ayon_substancepainter --- .../ayon_substancepainter/deploy/plugins/openpype_plugin.py | 2 +- .../ayon_substancepainter/plugins/create/create_textures.py | 4 ++-- .../ayon_substancepainter/plugins/create/create_workfile.py | 2 +- .../client/ayon_substancepainter/plugins/load/load_mesh.py | 2 +- .../plugins/publish/collect_textureset_images.py | 2 +- server_addon/substancepainter/package.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py index 8ced463367..5a0aff5649 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py @@ -22,7 +22,7 @@ def cleanup_openpype_qt_widgets(): def start_plugin(): from ayon_core.pipeline import install_host - from ayon_core.hosts.substancepainter.api import SubstanceHost + from ayon_substancepainter.api import SubstanceHost install_host(SubstanceHost()) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py index f46afadb5a..248bc00aa3 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py @@ -9,13 +9,13 @@ from ayon_core.lib import ( BoolDef ) -from ayon_core.hosts.substancepainter.api.pipeline import ( +from ayon_substancepainter.api.pipeline import ( get_instances, set_instance, set_instances, remove_instance ) -from ayon_core.hosts.substancepainter.api.lib import get_export_presets +from ayon_substancepainter.api.lib import get_export_presets import substance_painter.project diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py index 63b1c6c7da..16a6f1bbb8 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py @@ -5,7 +5,7 @@ import ayon_api from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.hosts.substancepainter.api.pipeline import ( +from ayon_substancepainter.api.pipeline import ( set_instances, set_instance, get_instances diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py index d5aac1191c..e2a48dd5a4 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/load/load_mesh.py @@ -5,7 +5,7 @@ from ayon_core.pipeline import ( get_representation_path, ) from ayon_core.pipeline.load import LoadError -from ayon_core.hosts.substancepainter.api.pipeline import ( +from ayon_substancepainter.api.pipeline import ( imprint_container, set_container_metadata, remove_container_metadata diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index 20aaa56993..824f2d999f 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -6,7 +6,7 @@ import ayon_api import substance_painter.textureset from ayon_core.pipeline import publish -from ayon_core.hosts.substancepainter.api.lib import ( +from ayon_substancepainter.api.lib import ( get_parsed_export_maps, strip_template ) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index 66638b0a2e..7232a4dbe4 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -1,6 +1,6 @@ name = "substancepainter" title = "Substance Painter" -version = "0.1.2" +version = "0.2.0" client_dir = "ayon_substancepainter" From 9ee4de380d38a9d586b92e1d4863c40526e13fe8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 17:21:42 +0800 Subject: [PATCH 07/65] added addon milestone version --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 21b1193b07..39dcb6b044 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -51,6 +51,7 @@ IGNORED_MODULES_IN_AYON = set() # - this is used to log the missing addon MOVED_ADDON_MILESTONE_VERSIONS = { "applications": VersionInfo(0, 2, 0), + "substancepainter": VersionInfo(0, 2, 0), } # Inherit from `object` for Python 2 hosts From b2215217e408e6b1643eb49938069bc312c6a9c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 17:34:24 +0800 Subject: [PATCH 08/65] add version.py --- .../substancepainter/client/ayon_substancepainter/version.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 server_addon/substancepainter/client/ayon_substancepainter/version.py diff --git a/server_addon/substancepainter/client/ayon_substancepainter/version.py b/server_addon/substancepainter/client/ayon_substancepainter/version.py new file mode 100644 index 0000000000..4f4a309b30 --- /dev/null +++ b/server_addon/substancepainter/client/ayon_substancepainter/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring Substance Painter addon version.""" +__version__ = "0.2.0" \ No newline at end of file From f476de6e493fac21622cc2919a004711d75d96d1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 17:41:57 +0800 Subject: [PATCH 09/65] add version.py --- .../substancepainter/client/ayon_substancepainter/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/version.py b/server_addon/substancepainter/client/ayon_substancepainter/version.py index 4f4a309b30..b4293649fe 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/version.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring Substance Painter addon version.""" -__version__ = "0.2.0" \ No newline at end of file +"""Package declaring AYON core addon version.""" +__version__ = "0.2.0" From 33d491e1f6afc600b64effa88dfedee600cafeb9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 19:19:07 +0800 Subject: [PATCH 10/65] add version.py --- .../substancepainter/client/ayon_substancepainter/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/version.py b/server_addon/substancepainter/client/ayon_substancepainter/version.py index b4293649fe..36fe942532 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/version.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring AYON core addon version.""" +"""Package declaring Substance Painter addon version.""" __version__ = "0.2.0" From bec84133e2d8c2c88eb14a3651443a16c7867a18 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 May 2024 20:30:27 +0800 Subject: [PATCH 11/65] rename Openpype to AYON --- .../client/ayon_substancepainter/api/pipeline.py | 2 +- .../plugins/{openpype_plugin.py => ayon_plugin.py} | 12 ++++++------ ...oad_on_first_run.py => ayon_load_on_first_run.py} | 4 ++-- .../plugins/create/create_textures.py | 1 + .../plugins/create/create_workfile.py | 1 + .../client/ayon_substancepainter/version.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) rename server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/{openpype_plugin.py => ayon_plugin.py} (73%) rename server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/{openpype_load_on_first_run.py => ayon_load_on_first_run.py} (93%) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py b/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py index 6e0ca0145b..47090c4b0a 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/api/pipeline.py @@ -31,7 +31,7 @@ from ayon_substancepainter import SUBSTANCE_HOST_DIR from . import lib -log = logging.getLogger("ayon_core.hosts.substance") +log = logging.getLogger("ayon_substancepainter") PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py similarity index 73% rename from server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py rename to server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py index 5a0aff5649..1cdcc5073d 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/openpype_plugin.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py @@ -1,6 +1,6 @@ -def cleanup_openpype_qt_widgets(): +def cleanup_ayon_qt_widgets(): """ Workaround for Substance failing to shut down correctly when a Qt window was still open at the time of shutting down. @@ -8,14 +8,14 @@ def cleanup_openpype_qt_widgets(): This seems to work sometimes, but not all the time. """ - # TODO: Create a more reliable method to close down all OpenPype Qt widgets + # TODO: Create a more reliable method to close down all AYON Qt widgets from PySide2 import QtWidgets import substance_painter.ui - # Kill OpenPype Qt widgets - print("Killing OpenPype Qt widgets..") + # Kill AYON Qt widgets + print("Killing AYON Qt widgets..") for widget in QtWidgets.QApplication.topLevelWidgets(): - if widget.__module__.startswith("openpype."): + if widget.__module__.startswith("ayon."): print(f"Deleting widget: {widget.__class__.__name__}") substance_painter.ui.delete_ui_element(widget) @@ -28,7 +28,7 @@ def start_plugin(): def close_plugin(): from ayon_core.pipeline import uninstall_host - cleanup_openpype_qt_widgets() + cleanup_ayon_qt_widgets() uninstall_host() diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/openpype_load_on_first_run.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py similarity index 93% rename from server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/openpype_load_on_first_run.py rename to server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py index 04b610b4df..666a0e9f05 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/openpype_load_on_first_run.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py @@ -1,6 +1,6 @@ -"""Ease the OpenPype on-boarding process by loading the plug-in on first run""" +"""Ease the AYON on-boarding process by loading the plug-in on first run""" -OPENPYPE_PLUGIN_NAME = "openpype_plugin" +OPENPYPE_PLUGIN_NAME = "ayon_plugin" def start_plugin(): diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py index 248bc00aa3..45c171fb23 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_textures.py @@ -28,6 +28,7 @@ class CreateTextures(Creator): icon = "picture-o" default_variant = "Main" + settings_category = "substancepainter" def create(self, product_name, instance_data, pre_create_data): diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py index 16a6f1bbb8..b100e4189d 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py @@ -22,6 +22,7 @@ class CreateWorkfile(AutoCreator): icon = "document" default_variant = "Main" + settings_category = "substancepainter" def create(self): diff --git a/server_addon/substancepainter/client/ayon_substancepainter/version.py b/server_addon/substancepainter/client/ayon_substancepainter/version.py index 36fe942532..b4293649fe 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/version.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring Substance Painter addon version.""" +"""Package declaring AYON core addon version.""" __version__ = "0.2.0" From 2ac32e71855d08c8596db4500f777742df44eb86 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 15 May 2024 20:44:54 +0800 Subject: [PATCH 12/65] Update server_addon/substancepainter/package.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/substancepainter/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index 7232a4dbe4..1508225fd8 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -5,6 +5,6 @@ version = "0.2.0" client_dir = "ayon_substancepainter" ayon_required_addons = { - "core": ">=0.3.1", + "core": ">0.3.1", } ayon_compatible_addons = {} From 498260172a4dc50f06cee6aaf0296ad60401f66a Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Mon, 20 May 2024 22:18:01 +0800 Subject: [PATCH 13/65] Update server_addon/substancepainter/package.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/substancepainter/package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index 1508225fd8..b82168c27e 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -1,6 +1,7 @@ name = "substancepainter" title = "Substance Painter" version = "0.2.0" +app_host_name = "substancepainter" client_dir = "ayon_substancepainter" From 31c24e4129e9323eea6b78de5068115eb13dd161 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 May 2024 00:45:13 +0800 Subject: [PATCH 14/65] make sure module name to find QtWIdget is correct and make sure all Openpype name converted to AYON --- .../deploy/plugins/ayon_plugin.py | 3 ++- .../deploy/startup/ayon_load_on_first_run.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py index 1cdcc5073d..fd819bf476 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py @@ -15,7 +15,8 @@ def cleanup_ayon_qt_widgets(): # Kill AYON Qt widgets print("Killing AYON Qt widgets..") for widget in QtWidgets.QApplication.topLevelWidgets(): - if widget.__module__.startswith("ayon."): + print(widget.__module__) + if widget.__module__.startswith("ayon_substancepainter."): print(f"Deleting widget: {widget.__class__.__name__}") substance_painter.ui.delete_ui_element(widget) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py index 666a0e9f05..9ef119e357 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/startup/ayon_load_on_first_run.py @@ -1,6 +1,6 @@ """Ease the AYON on-boarding process by loading the plug-in on first run""" -OPENPYPE_PLUGIN_NAME = "ayon_plugin" +AYON_PLUGIN_NAME = "ayon_plugin" def start_plugin(): @@ -19,9 +19,9 @@ def start_plugin(): # later than this startup script, we check whether its menu initialized is_before_plugins_menu = PLUGINS_MENU is None - settings = get_settings(OPENPYPE_PLUGIN_NAME) + settings = get_settings(AYON_PLUGIN_NAME) if settings.value(LAUNCH_AT_START_KEY, None) is None: - print("Initializing OpenPype plug-in on first run...") + print("Initializing AYON plug-in on first run...") if is_before_plugins_menu: print("- running before 'painter_plugins_ui'") # Delay the launch to the painter_plugins_ui initialization @@ -29,11 +29,11 @@ def start_plugin(): else: # Launch now print("- running after 'painter_plugins_ui'") - plugin_manager(OPENPYPE_PLUGIN_NAME)(True) + plugin_manager(AYON_PLUGIN_NAME)(True) # Set the checked state in the menu to avoid confusion action = next(action for action in PLUGINS_MENU._menu.actions() - if action.text() == OPENPYPE_PLUGIN_NAME) + if action.text() == AYON_PLUGIN_NAME) if action is not None: action.blockSignals(True) action.setChecked(True) From 22564b2071dc25acdf0afe8b17e2b65e6c597365 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 23 May 2024 00:56:34 +0800 Subject: [PATCH 15/65] Update server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../client/ayon_substancepainter/deploy/plugins/ayon_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py index fd819bf476..4d7b318e44 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py @@ -15,7 +15,6 @@ def cleanup_ayon_qt_widgets(): # Kill AYON Qt widgets print("Killing AYON Qt widgets..") for widget in QtWidgets.QApplication.topLevelWidgets(): - print(widget.__module__) if widget.__module__.startswith("ayon_substancepainter."): print(f"Deleting widget: {widget.__class__.__name__}") substance_painter.ui.delete_ui_element(widget) From b2c0fc08b7989975ec1b97832ee61a87d1ff13a9 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 23 May 2024 00:57:01 +0800 Subject: [PATCH 16/65] Update server_addon/substancepainter/package.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/substancepainter/package.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index b82168c27e..1508225fd8 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -1,7 +1,6 @@ name = "substancepainter" title = "Substance Painter" version = "0.2.0" -app_host_name = "substancepainter" client_dir = "ayon_substancepainter" From 11eb3d7def6c7faf2510f0f94cd4d4b966e64f70 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 23 May 2024 01:00:05 +0800 Subject: [PATCH 17/65] use ayon_ to capture widget in ayon_core too --- .../client/ayon_substancepainter/deploy/plugins/ayon_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py index 4d7b318e44..85bb56d73c 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/deploy/plugins/ayon_plugin.py @@ -15,7 +15,7 @@ def cleanup_ayon_qt_widgets(): # Kill AYON Qt widgets print("Killing AYON Qt widgets..") for widget in QtWidgets.QApplication.topLevelWidgets(): - if widget.__module__.startswith("ayon_substancepainter."): + if widget.__module__.startswith("ayon_"): print(f"Deleting widget: {widget.__class__.__name__}") substance_painter.ui.delete_ui_element(widget) From c2984fcc3b97323a75af48b4396bb595bac0ac9a Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 23 May 2024 21:08:13 +0800 Subject: [PATCH 18/65] Update server_addon/substancepainter/package.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/substancepainter/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/substancepainter/package.py b/server_addon/substancepainter/package.py index 1508225fd8..4db5ef634f 100644 --- a/server_addon/substancepainter/package.py +++ b/server_addon/substancepainter/package.py @@ -5,6 +5,6 @@ version = "0.2.0" client_dir = "ayon_substancepainter" ayon_required_addons = { - "core": ">0.3.1", + "core": ">0.3.2", } ayon_compatible_addons = {} From 86581bdd44ff02cbfe720a65d71a4da4f952beaf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 17:47:06 +0200 Subject: [PATCH 19/65] add created by information to items --- client/ayon_core/tools/workfiles/abstract.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index c9eb9004e3..b45c643d97 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -13,8 +13,9 @@ class WorkfileInfo: task_id (str): Task id. filepath (str): Filepath. filesize (int): File size. - creation_time (int): Creation time (timestamp). - modification_time (int): Modification time (timestamp). + creation_time (float): Creation time (timestamp). + modification_time (float): Modification time (timestamp). + created_by (Union[str, none]): User who created the file. note (str): Note. """ @@ -26,6 +27,7 @@ class WorkfileInfo: filesize, creation_time, modification_time, + created_by, note, ): self.folder_id = folder_id @@ -34,6 +36,7 @@ class WorkfileInfo: self.filesize = filesize self.creation_time = creation_time self.modification_time = modification_time + self.created_by = created_by self.note = note def to_data(self): @@ -50,6 +53,7 @@ class WorkfileInfo: "filesize": self.filesize, "creation_time": self.creation_time, "modification_time": self.modification_time, + "created_by": self.created_by, "note": self.note, } @@ -212,6 +216,7 @@ class FileItem: dirpath (str): Directory path of file. filename (str): Filename. modified (float): Modified timestamp. + created_by (Optional[str]): Username. representation_id (Optional[str]): Representation id of published workfile. filepath (Optional[str]): Prepared filepath. @@ -223,6 +228,7 @@ class FileItem: dirpath, filename, modified, + created_by=None, representation_id=None, filepath=None, exists=None @@ -230,6 +236,7 @@ class FileItem: self.filename = filename self.dirpath = dirpath self.modified = modified + self.created_by = created_by self.representation_id = representation_id self._filepath = filepath self._exists = exists @@ -269,6 +276,7 @@ class FileItem: "filename": self.filename, "dirpath": self.dirpath, "modified": self.modified, + "created_by": self.created_by, "representation_id": self.representation_id, "filepath": self.filepath, "exists": self.exists, From 70298b878d51a11cf13b1e7f635c67d4ef300af3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 17:50:50 +0200 Subject: [PATCH 20/65] fill created by information --- .../tools/workfiles/models/workfiles.py | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 5f59b99b22..40b56ed99c 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -188,10 +188,16 @@ class WorkareaModel: if ext not in self._extensions: continue - modified = os.path.getmtime(filepath) - items.append( - FileItem(workdir, filename, modified) + workfile_info = self._controller.get_workfile_info( + folder_id, task_id, filepath ) + modified = os.path.getmtime(filepath) + items.append(FileItem( + workdir, + filename, + modified, + workfile_info.created_by + )) return items def _get_template_key(self, fill_data): @@ -459,8 +465,10 @@ class WorkfileEntitiesModel: self, folder_id, task_id, workfile_info, filepath ): note = "" + created_by = None if workfile_info: note = workfile_info["attrib"].get("description") or "" + created_by = workfile_info.get("createdBy") filestat = os.stat(filepath) return WorkfileInfo( @@ -470,6 +478,7 @@ class WorkfileEntitiesModel: filesize=filestat.st_size, creation_time=filestat.st_ctime, modification_time=filestat.st_mtime, + created_by=created_by, note=note ) @@ -481,7 +490,7 @@ class WorkfileEntitiesModel: for workfile_info in ayon_api.get_workfiles_info( self._controller.get_current_project_name(), task_ids=[task_id], - fields=["id", "path", "attrib"], + fields=["id", "path", "attrib", "createdBy"], ): workfile_identifier = self._get_workfile_info_identifier( folder_id, task_id, workfile_info["path"] @@ -599,7 +608,7 @@ class PublishWorkfilesModel: return self._cached_repre_extensions def _file_item_from_representation( - self, repre_entity, project_anatomy, task_name=None + self, repre_entity, project_anatomy, author, task_name=None ): if task_name is not None: task_info = repre_entity["context"].get("task") @@ -634,6 +643,7 @@ class PublishWorkfilesModel: dirpath, filename, created_at.float_timestamp, + author, repre_entity["id"] ) @@ -643,9 +653,9 @@ class PublishWorkfilesModel: # Get subset docs of folder product_entities = ayon_api.get_products( project_name, - folder_ids=[folder_id], - product_types=["workfile"], - fields=["id", "name"] + folder_ids={folder_id}, + product_types={"workfile"}, + fields={"id", "name"} ) output = [] @@ -657,25 +667,30 @@ class PublishWorkfilesModel: version_entities = ayon_api.get_versions( project_name, product_ids=product_ids, - fields=["id", "productId"] + fields={"id", "author"} ) - version_ids = {version["id"] for version in version_entities} - if not version_ids: + versions_by_id = {version["id"]: version for version in version_entities} + if not versions_by_id: return output # Query representations of filtered versions and add filter for # extension repre_entities = ayon_api.get_representations( project_name, - version_ids=version_ids + version_ids=set(versions_by_id) ) project_anatomy = self._controller.project_anatomy # Filter queried representations by task name if task is set file_items = [] for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = versions_by_id[version_id] file_item = self._file_item_from_representation( - repre_entity, project_anatomy, task_name + repre_entity, + project_anatomy, + task_name, + version_entity["author"] ) if file_item is not None: file_items.append(file_item) From ef4e779e06153aa4d2378de4fb049c1b7e8f045d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 17:53:31 +0200 Subject: [PATCH 21/65] fill createdBy and updateBy --- .../tools/workfiles/models/workfiles.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 40b56ed99c..ca7654846b 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -6,6 +6,7 @@ import arrow import ayon_api from ayon_api.operations import OperationsSession +from ayon_core.lib import get_ayon_username from ayon_core.pipeline.template_data import ( get_template_data, get_task_template_data, @@ -23,6 +24,8 @@ from ayon_core.tools.workfiles.abstract import ( WorkfileInfo, ) +_NOT_SET = object() + class CommentMatcher(object): """Use anatomy and work file data to parse comments from filenames. @@ -445,6 +448,7 @@ class WorkfileEntitiesModel: self._controller = controller self._cache = {} self._items = {} + self._current_username = _NOT_SET def _get_workfile_info_identifier( self, folder_id, task_id, rootless_path @@ -563,13 +567,18 @@ class WorkfileEntitiesModel: project_name = self._controller.get_current_project_name() + username = self._get_current_username() workfile_info = { "path": rootless_path, "taskId": task_id, "attrib": { "extension": extension, "description": note - } + }, + # TODO remove 'createdBy' and 'updatedBy' fields when server is + # or above 1.1.3 . + "createdBy": username, + "updatedBy": username, } session = OperationsSession() @@ -577,6 +586,11 @@ class WorkfileEntitiesModel: session.commit() return workfile_info + def _get_current_username(self): + if self._current_username is _NOT_SET: + self._current_username = get_ayon_username() + return self._current_username + class PublishWorkfilesModel: """Model for handling of published workfiles. From d4d07e581f946e643716ed479add3961c29dfdff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 17:54:38 +0200 Subject: [PATCH 22/65] autofix older workfile info --- .../tools/workfiles/models/workfiles.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index ca7654846b..76dd62c9c0 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -538,18 +538,32 @@ class WorkfileEntitiesModel: self._items.pop(identifier, None) return - if note is None: - return - old_note = workfile_info.get("attrib", {}).get("note") new_workfile_info = copy.deepcopy(workfile_info) - attrib = new_workfile_info.setdefault("attrib", {}) - attrib["description"] = note + update_data = {} + if note is not None and old_note != note: + update_data["attrib"] = {"description": note} + attrib = new_workfile_info.setdefault("attrib", {}) + attrib["description"] = note + + username = self._get_current_username() + # Automatically fix 'createdBy' and 'updatedBy' fields + # NOTE both fields were not automatically filled by server + # until 1.1.3 release. + if workfile_info.get("createdBy") is None: + update_data["createdBy"] = username + new_workfile_info["createdBy"] = username + + if workfile_info.get("updatedBy") != username: + update_data["updatedBy"] = username + new_workfile_info["updatedBy"] = username + + if not update_data: + return + self._cache[identifier] = new_workfile_info self._items.pop(identifier, None) - if old_note == note: - return project_name = self._controller.get_current_project_name() @@ -558,7 +572,7 @@ class WorkfileEntitiesModel: project_name, "workfile", workfile_info["id"], - {"attrib": {"description": note}}, + update_data, ) session.commit() From 663e5cb0aa330d93634985335b161f2e8f50ba30 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 18:24:11 +0200 Subject: [PATCH 23/65] fix args order --- client/ayon_core/tools/workfiles/models/workfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 76dd62c9c0..6a69a19f9c 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -717,8 +717,8 @@ class PublishWorkfilesModel: file_item = self._file_item_from_representation( repre_entity, project_anatomy, + version_entity["author"], task_name, - version_entity["author"] ) if file_item is not None: file_items.append(file_item) From 5280a72ea8d7eae4244de0dd1b89f1ad974c2a61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 18:24:19 +0200 Subject: [PATCH 24/65] fix formatting --- client/ayon_core/tools/workfiles/models/workfiles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 6a69a19f9c..bf30b5dfc2 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -697,7 +697,10 @@ class PublishWorkfilesModel: product_ids=product_ids, fields={"id", "author"} ) - versions_by_id = {version["id"]: version for version in version_entities} + versions_by_id = { + version["id"]: version + for version in version_entities + } if not versions_by_id: return output From 49bbd46727fc7301ca5e6d2e1b5e274df5e081ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 May 2024 18:24:52 +0200 Subject: [PATCH 25/65] use data in views --- .../widgets/files_widget_published.py | 42 +++++++++++++------ .../widgets/files_widget_workarea.py | 40 ++++++++++++------ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py index bf36d790e9..5f2669bb48 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py @@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame REPRE_ID_ROLE = QtCore.Qt.UserRole + 1 FILEPATH_ROLE = QtCore.Qt.UserRole + 2 -DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 +AUTHOR_ROLE = QtCore.Qt.UserRole + 3 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4 class PublishedFilesModel(QtGui.QStandardItemModel): @@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel): controller (AbstractWorkfilesFrontend): The control object. """ + columns = [ + "Name", + "Author", + "Date Modified", + ] + date_modified_col = columns.index("Date Modified") + def __init__(self, controller): super(PublishedFilesModel, self).__init__() - self.setColumnCount(2) - - self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") - self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + self.setColumnCount(len(self.columns)) + for idx, label in enumerate(self.columns): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) controller.register_event_callback( "selection.task.changed", @@ -207,6 +214,7 @@ class PublishedFilesModel(QtGui.QStandardItemModel): item.setFlags(flags) item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.created_by, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_id[repre_id] = item @@ -225,22 +233,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel): # Use flags of first column for all columns if index.column() != 0: index = self.index(index.row(), 0, index.parent()) - return super(PublishedFilesModel, self).flags(index) + return super().flags(index) def data(self, index, role=None): if role is None: role = QtCore.Qt.DisplayRole # Handle roles for first column - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None + col = index.column() + if col != 1: + return super().data(index, role) - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if col == 1: + role = AUTHOR_ROLE + elif col == 2: role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) + else: + return None + index = self.index(index.row(), 0, index.parent()) - return super(PublishedFilesModel, self).data(index, role) + return super().data(index, role) class SelectContextOverlay(BaseOverlayFrame): @@ -295,7 +311,7 @@ class PublishedFilesWidget(QtWidgets.QWidget): view.setModel(proxy_model) time_delegate = PrettyTimeDelegate() - view.setItemDelegateForColumn(1, time_delegate) + view.setItemDelegateForColumn(model.date_modified_col, time_delegate) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index fe6abee951..7b316adf81 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -10,7 +10,8 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate FILENAME_ROLE = QtCore.Qt.UserRole + 1 FILEPATH_ROLE = QtCore.Qt.UserRole + 2 -DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 +AUTHOR_ROLE = QtCore.Qt.UserRole + 3 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4 class WorkAreaFilesModel(QtGui.QStandardItemModel): @@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): """ refreshed = QtCore.Signal() + columns = [ + "Name", + "Author", + "Date Modified", + ] + date_modified_col = columns.index("Date Modified") def __init__(self, controller): super(WorkAreaFilesModel, self).__init__() - self.setColumnCount(2) + self.setColumnCount(len(self.columns)) - self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") - self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + for idx, label in enumerate(self.columns): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) controller.register_event_callback( "selection.folder.changed", @@ -206,6 +213,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): item.setData(file_item.filename, FILENAME_ROLE) item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.created_by, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_filename[file_item.filename] = item @@ -224,22 +232,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): # Use flags of first column for all columns if index.column() != 0: index = self.index(index.row(), 0, index.parent()) - return super(WorkAreaFilesModel, self).flags(index) + return super().flags(index) def data(self, index, role=None): if role is None: role = QtCore.Qt.DisplayRole # Handle roles for first column - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None + col = index.column() + if col == 0: + return super().data(index, role) - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if col == 1: + role = AUTHOR_ROLE + elif col == 2: role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) + else: + return None + index = self.index(index.row(), 0, index.parent()) - return super(WorkAreaFilesModel, self).data(index, role) + return super().data(index, role) def set_published_mode(self, published_mode): if self._published_mode == published_mode: @@ -279,7 +295,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): view.setModel(proxy_model) time_delegate = PrettyTimeDelegate() - view.setItemDelegateForColumn(1, time_delegate) + view.setItemDelegateForColumn(model.date_modified_col, time_delegate) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. From 91b52820b32a8b319faedac1edbfe7b5681cd0fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 May 2024 11:46:41 +0200 Subject: [PATCH 26/65] prepared generic model 'PluginStateByHostModel' --- server/settings/publish_plugins.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 61e73ce912..9e6fc760b0 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -59,7 +59,7 @@ class CollectFramesFixDefModel(BaseSettingsModel): ) -class ValidateOutdatedContainersProfile(BaseSettingsModel): +class PluginStateByHostModelProfile(BaseSettingsModel): _layout = "expanded" # Filtering host_names: list[str] = SettingsField( @@ -72,15 +72,9 @@ class ValidateOutdatedContainersProfile(BaseSettingsModel): active: bool = SettingsField(True, title="Active") -class ValidateOutdatedContainersModel(BaseSettingsModel): - """Validate if Publishing intent was selected. - - It is possible to disable validation for specific publishing context - with profiles. - """ - +class PluginStateByHostModel(BaseSettingsModel): _isGroup = True - plugin_state_profiles: list[ValidateOutdatedContainersProfile] = SettingsField( + plugin_state_profiles: list[PluginStateByHostModelProfile] = SettingsField( default_factory=list, title="Plugin enable state profiles", ) @@ -797,8 +791,8 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidateBaseModel, title="Validate Version" ) - ValidateOutdatedContainers: ValidateOutdatedContainersModel = SettingsField( - default_factory=ValidateOutdatedContainersModel, + ValidateOutdatedContainers: PluginStateByHostModel = SettingsField( + default_factory=PluginStateByHostModel, title="Validate Containers" ) ValidateIntent: ValidateIntentModel = SettingsField( From 59547099100418c621a37844b2b900cbb603c158 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 May 2024 11:47:00 +0200 Subject: [PATCH 27/65] validate version is enabled, optional, active based on profiles --- .../plugins/publish/validate_version.py | 33 +++++++++++++++++-- server/settings/publish_plugins.py | 22 ++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py index 25a5757330..c2f7d5bf44 100644 --- a/client/ayon_core/plugins/publish/validate_version.py +++ b/client/ayon_core/plugins/publish/validate_version.py @@ -1,6 +1,10 @@ import pyblish.api + +from ayon_core.lib import filter_profiles from ayon_core.pipeline.publish import ( - PublishValidationError, OptionalPyblishPluginMixin + PublishValidationError, + OptionalPyblishPluginMixin, + get_current_host_name, ) @@ -13,12 +17,35 @@ class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): order = pyblish.api.ValidatorOrder label = "Validate Version" - hosts = ["nuke", "maya", "houdini", "blender", - "photoshop", "aftereffects"] optional = False active = True + @classmethod + def apply_settings(cls, settings): + # Disable if no profile is found for the current host + profiles = ( + settings + ["core"] + ["publish"] + ["ValidateVersion"] + ["plugin_state_profiles"] + ) + profile = filter_profiles( + profiles, {"host_names": get_current_host_name()} + ) + if not profile: + cls.enabled = False + return + + # Apply settings from profile + for attr_name in { + "enabled", + "optional", + "active", + }: + setattr(cls, attr_name, profile[attr_name]) + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 9e6fc760b0..11f615c6f4 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -787,8 +787,8 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidateBaseModel, title="Validate Editorial Asset Name" ) - ValidateVersion: ValidateBaseModel = SettingsField( - default_factory=ValidateBaseModel, + ValidateVersion: PluginStateByHostModel = SettingsField( + default_factory=PluginStateByHostModel, title="Validate Version" ) ValidateOutdatedContainers: PluginStateByHostModel = SettingsField( @@ -876,9 +876,21 @@ DEFAULT_PUBLISH_VALUES = { "active": True }, "ValidateVersion": { - "enabled": True, - "optional": False, - "active": True + "plugin_state_profiles": [ + { + "host_names": [ + "aftereffects", + "blender", + "houdini", + "maya", + "nuke", + "photoshop", + ], + "enabled": True, + "optional": False, + "active": True + } + ] }, "ValidateOutdatedContainers": { "plugin_state_profiles": [ From 57384baf6b50d1de95ca8c90e86ed6fd073730bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 May 2024 12:14:07 +0200 Subject: [PATCH 28/65] added description to plugin --- server/settings/publish_plugins.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 11f615c6f4..ef531c8345 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -77,6 +77,7 @@ class PluginStateByHostModel(BaseSettingsModel): plugin_state_profiles: list[PluginStateByHostModelProfile] = SettingsField( default_factory=list, title="Plugin enable state profiles", + description="Change plugin state based on host name." ) @@ -789,7 +790,11 @@ class PublishPuginsModel(BaseSettingsModel): ) ValidateVersion: PluginStateByHostModel = SettingsField( default_factory=PluginStateByHostModel, - title="Validate Version" + title="Validate Version", + description=( + "Validate that product version to integrate" + " is newer than latest version in AYON." + ) ) ValidateOutdatedContainers: PluginStateByHostModel = SettingsField( default_factory=PluginStateByHostModel, From ac995dffc1368681871c1ec24edecaabc6f3e023 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 May 2024 12:17:13 +0200 Subject: [PATCH 29/65] moved conversion functions to settings --- server/__init__.py | 47 ++++--------------------------- server/settings/__init__.py | 3 ++ server/settings/conversion.py | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 41 deletions(-) create mode 100644 server/settings/conversion.py diff --git a/server/__init__.py b/server/__init__.py index 79f505ccd5..d60f50f471 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -2,7 +2,11 @@ from typing import Any from ayon_server.addons import BaseServerAddon -from .settings import CoreSettings, DEFAULT_VALUES +from .settings import ( + CoreSettings, + DEFAULT_VALUES, + convert_settings_overrides, +) class CoreAddon(BaseServerAddon): @@ -17,47 +21,8 @@ class CoreAddon(BaseServerAddon): source_version: str, overrides: dict[str, Any], ) -> dict[str, Any]: - self._convert_imagio_configs_0_3_1(overrides) + convert_settings_overrides(source_version, overrides) # Use super conversion return await super().convert_settings_overrides( source_version, overrides ) - - def _convert_imagio_configs_0_3_1(self, overrides): - """Imageio config settings did change to profiles since 0.3.1. .""" - imageio_overrides = overrides.get("imageio") or {} - if ( - "ocio_config" not in imageio_overrides - or "filepath" not in imageio_overrides["ocio_config"] - ): - return - - ocio_config = imageio_overrides.pop("ocio_config") - - filepath = ocio_config["filepath"] - if not filepath: - return - first_filepath = filepath[0] - ocio_config_profiles = imageio_overrides.setdefault( - "ocio_config_profiles", [] - ) - base_value = { - "type": "builtin_path", - "product_name": "", - "host_names": [], - "task_names": [], - "task_types": [], - "custom_path": "", - "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio" - } - if first_filepath in ( - "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", - "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio", - ): - base_value["type"] = "builtin_path" - base_value["builtin_path"] = first_filepath - else: - base_value["type"] = "custom_path" - base_value["custom_path"] = first_filepath - - ocio_config_profiles.append(base_value) diff --git a/server/settings/__init__.py b/server/settings/__init__.py index 527a2bdc0c..4bb21a9644 100644 --- a/server/settings/__init__.py +++ b/server/settings/__init__.py @@ -1,7 +1,10 @@ from .main import CoreSettings, DEFAULT_VALUES +from .conversion import convert_settings_overrides __all__ = ( "CoreSettings", "DEFAULT_VALUES", + + "convert_settings_overrides", ) diff --git a/server/settings/conversion.py b/server/settings/conversion.py new file mode 100644 index 0000000000..74046fe020 --- /dev/null +++ b/server/settings/conversion.py @@ -0,0 +1,52 @@ +import copy +from typing import Any + +from .publish_plugins import DEFAULT_PUBLISH_VALUES + + +def _convert_imageio_configs_0_3_1(overrides): + """Imageio config settings did change to profiles since 0.3.1. .""" + imageio_overrides = overrides.get("imageio") or {} + if ( + "ocio_config" not in imageio_overrides + or "filepath" not in imageio_overrides["ocio_config"] + ): + return + + ocio_config = imageio_overrides.pop("ocio_config") + + filepath = ocio_config["filepath"] + if not filepath: + return + first_filepath = filepath[0] + ocio_config_profiles = imageio_overrides.setdefault( + "ocio_config_profiles", [] + ) + base_value = { + "type": "builtin_path", + "product_name": "", + "host_names": [], + "task_names": [], + "task_types": [], + "custom_path": "", + "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio" + } + if first_filepath in ( + "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio", + ): + base_value["type"] = "builtin_path" + base_value["builtin_path"] = first_filepath + else: + base_value["type"] = "custom_path" + base_value["custom_path"] = first_filepath + + ocio_config_profiles.append(base_value) + + +def convert_settings_overrides( + source_version: str, + overrides: dict[str, Any], +) -> dict[str, Any]: + _convert_imageio_configs_0_3_1(overrides) + return overrides From c6123d257d8608391b3593cdb9934ceb6adbcbd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 May 2024 12:18:58 +0200 Subject: [PATCH 30/65] added conversion for ValidateVersion settings --- server/settings/conversion.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 74046fe020..f513738603 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -44,9 +44,43 @@ def _convert_imageio_configs_0_3_1(overrides): ocio_config_profiles.append(base_value) +def _convert_validate_version_0_3_3(publish_overrides): + """ValidateVersion plugin changed in 0.3.3.""" + if "ValidateVersion" not in publish_overrides: + return + + validate_version = publish_overrides["ValidateVersion"] + # Already new settings + if "plugin_state_profiles" in validate_version: + return + + # Use new default profile as base + profile = copy.deepcopy( + DEFAULT_PUBLISH_VALUES["ValidateVersion"]["plugin_state_profiles"][0] + ) + # Copy values from old overrides to new overrides + for key in { + "enabled", + "optional", + "active", + }: + if key not in validate_version: + continue + profile[key] = validate_version.pop(key) + + validate_version["plugin_state_profiles"] = [profile] + + +def _conver_publish_plugins(overrides): + if "publish" not in overrides: + return + _convert_validate_version_0_3_3(overrides["publish"]) + + def convert_settings_overrides( source_version: str, overrides: dict[str, Any], ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) + _conver_publish_plugins(overrides) return overrides From 3b2901772ed464ae0ae84e658c2ae0d07126c8c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 May 2024 22:10:57 +0800 Subject: [PATCH 31/65] remove unused version.py in the client folder --- .../substancepainter/client/ayon_substancepainter/version.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 server_addon/substancepainter/client/ayon_substancepainter/version.py diff --git a/server_addon/substancepainter/client/ayon_substancepainter/version.py b/server_addon/substancepainter/client/ayon_substancepainter/version.py deleted file mode 100644 index b4293649fe..0000000000 --- a/server_addon/substancepainter/client/ayon_substancepainter/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -"""Package declaring AYON core addon version.""" -__version__ = "0.2.0" From 59d84ad2c58f2ae8d41bd0601d94c125b019a373 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 May 2024 12:54:25 +0200 Subject: [PATCH 32/65] Update product type from "editorial_pckg" to "editorial_pkg" throughout the codebase. Fix typos and align identifiers, labels, and descriptions accordingly. --- .../plugins/load/load_editorial_package.py | 2 +- .../plugins/create/create_editorial_package.py | 12 ++++++------ .../publish/collect_editorial_package.py | 14 +++++++------- .../plugins/publish/extract_editorial_pckg.py | 18 +++++++++--------- .../publish/validate_editorial_package.py | 14 +++++++------- server_addon/traypublisher/package.py | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py index 9b723b0130..438f277775 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py @@ -15,7 +15,7 @@ class LoadEditorialPackage(load.LoaderPlugin): and timeline structure. """ - product_types = {"editorial_pckg"} + product_types = {"editorial_pkg"} representations = {"*"} extensions = {"otio"} diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py index 5f0a84be4a..bc003c0601 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py @@ -16,13 +16,13 @@ class EditorialPackageCreator(TrayPublishCreator): """Creates instance for OTIO file from published folder. Folder contains OTIO file and exported .mov files. Process should publish - whole folder as single `editorial_pckg` product type and (possibly) convert + whole folder as single `editorial_pkg` product type and (possibly) convert .mov files into different format and copy them into `publish` `resources` subfolder. """ - identifier = "editorial_pckg" + identifier = "editorial_pkg" label = "Editorial package" - product_type = "editorial_pckg" + product_type = "editorial_pkg" description = "Publish folder with OTIO file and resources" # Position batch creator after simple creators @@ -89,8 +89,8 @@ class EditorialPackageCreator(TrayPublishCreator): def get_detail_description(self): return """# Publish folder with OTIO file and video clips - Folder contains OTIO file and exported .mov files. Process should - publish whole folder as single `editorial_pckg` product type and - (possibly) convert .mov files into different format and copy them into + Folder contains OTIO file and exported .mov files. Process should + publish whole folder as single `editorial_pkg` product type and + (possibly) convert .mov files into different format and copy them into `publish` `resources` subfolder. """ diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py index cb1277546c..fb7d5cd5a1 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py @@ -1,10 +1,10 @@ -"""Produces instance.data["editorial_pckg"] data used during integration. +"""Produces instance.data["editorial_pkg"] data used during integration. Requires: instance.data["creator_attributes"]["path"] - from creator Provides: - instance -> editorial_pckg (dict): + instance -> editorial_pkg (dict): folder_path (str) otio_path (str) - from dragged folder resource_paths (list) @@ -24,7 +24,7 @@ class CollectEditorialPackage(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder - 0.1 hosts = ["traypublisher"] - families = ["editorial_pckg"] + families = ["editorial_pkg"] def process(self, instance): folder_path = instance.data["creator_attributes"]["folder_path"] @@ -34,14 +34,14 @@ class CollectEditorialPackage(pyblish.api.InstancePlugin): )) return - instance.data["editorial_pckg"] = {} - instance.data["editorial_pckg"]["folder_path"] = folder_path + instance.data["editorial_pkg"] = {} + instance.data["editorial_pkg"]["folder_path"] = folder_path otio_path, resource_paths = ( self._get_otio_and_resource_paths(folder_path)) - instance.data["editorial_pckg"]["otio_path"] = otio_path - instance.data["editorial_pckg"]["resource_paths"] = resource_paths + instance.data["editorial_pkg"]["otio_path"] = otio_path + instance.data["editorial_pkg"]["resource_paths"] = resource_paths def _get_otio_and_resource_paths(self, folder_path): otio_path = None diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py index 6dd4e84704..3e391b5f6e 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py @@ -20,27 +20,27 @@ class ExtractEditorialPckgConversion(publish.Extractor): label = "Extract Editorial Package" order = pyblish.api.ExtractorOrder - 0.45 hosts = ["traypublisher"] - families = ["editorial_pckg"] + families = ["editorial_pkg"] def process(self, instance): - editorial_pckg_data = instance.data.get("editorial_pckg") + editorial_pkg_data = instance.data.get("editorial_pkg") - otio_path = editorial_pckg_data["otio_path"] + otio_path = editorial_pkg_data["otio_path"] otio_basename = os.path.basename(otio_path) staging_dir = self.staging_dir(instance) - editorial_pckg_repre = { - 'name': "editorial_pckg", + editorial_pkg_repre = { + 'name': "editorial_pkg", 'ext': "otio", 'files': otio_basename, "stagingDir": staging_dir, } otio_staging_path = os.path.join(staging_dir, otio_basename) - instance.data["representations"].append(editorial_pckg_repre) + instance.data["representations"].append(editorial_pkg_repre) publish_resource_folder = self._get_publish_resource_folder(instance) - resource_paths = editorial_pckg_data["resource_paths"] + resource_paths = editorial_pkg_data["resource_paths"] transfers = self._get_transfers(resource_paths, publish_resource_folder) @@ -61,13 +61,13 @@ class ExtractEditorialPckgConversion(publish.Extractor): source_to_rootless = self._get_resource_path_mapping(instance, transfers) - otio_data = editorial_pckg_data["otio_data"] + otio_data = editorial_pkg_data["otio_data"] otio_data = self._replace_target_urls(otio_data, source_to_rootless) opentimelineio.adapters.write_to_file(otio_data, otio_staging_path) self.log.info("Added Editorial Package representation: {}".format( - editorial_pckg_repre)) + editorial_pkg_repre)) def _get_publish_resource_folder(self, instance): """Calculates publish folder and create it.""" diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py index c63c4a6a73..42755e1396 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py @@ -17,21 +17,21 @@ class ValidateEditorialPackage(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.49 hosts = ["traypublisher"] - families = ["editorial_pckg"] + families = ["editorial_pkg"] def process(self, instance): - editorial_pckg_data = instance.data.get("editorial_pckg") - if not editorial_pckg_data: + editorial_pkg_data = instance.data.get("editorial_pkg") + if not editorial_pkg_data: raise PublishValidationError("Editorial package not collected") - folder_path = editorial_pckg_data["folder_path"] + folder_path = editorial_pkg_data["folder_path"] - otio_path = editorial_pckg_data["otio_path"] + otio_path = editorial_pkg_data["otio_path"] if not otio_path: raise PublishValidationError( f"Folder {folder_path} missing otio file") - resource_paths = editorial_pckg_data["resource_paths"] + resource_paths = editorial_pkg_data["resource_paths"] resource_file_names = {os.path.basename(path) for path in resource_paths} @@ -50,7 +50,7 @@ class ValidateEditorialPackage(pyblish.api.InstancePlugin): f"Otio file contains missing files `{missing_files}`.\n\n" f"Please add them to `{folder_path}` and republish.") - instance.data["editorial_pckg"]["otio_data"] = otio_data + instance.data["editorial_pkg"]["otio_data"] = otio_data def _get_all_target_urls(self, otio_data): target_urls = [] diff --git a/server_addon/traypublisher/package.py b/server_addon/traypublisher/package.py index ea04835b45..c9b94c2b72 100644 --- a/server_addon/traypublisher/package.py +++ b/server_addon/traypublisher/package.py @@ -1,6 +1,6 @@ name = "traypublisher" title = "TrayPublisher" -version = "0.2.0" +version = "0.2.1" client_dir = "ayon_traypublisher" From 7e2647881130b5e976a20e97973581deda9f780e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 May 2024 13:58:37 +0200 Subject: [PATCH 33/65] Add functionality to create versioned bin for editorial package. Creates a new versioned bin using the name of the package and its version. --- .../hosts/resolve/plugins/load/load_editorial_package.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py index 438f277775..33395534fa 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_editorial_package.py @@ -32,6 +32,12 @@ class LoadEditorialPackage(load.LoaderPlugin): project = lib.get_current_project() media_pool = project.GetMediaPool() + + # create versioned bin for editorial package + version_name = context["version"]["name"] + bin_name = f"{name}_{version_name}" + lib.create_bin(bin_name) + import_options = { "timelineName": "Editorial Package Timeline", "importSourceClips": True, From 1ae7a22d1d5abb3659da18f4a6949aebbe66c97f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 May 2024 14:07:22 +0200 Subject: [PATCH 34/65] Add exception handling for unsupported schema in OTIO file. Improve error message and version requirement. --- .../plugins/publish/validate_editorial_package.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py index 42755e1396..02793516e2 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py @@ -1,5 +1,7 @@ import os import opentimelineio +from opentimelineio.exceptions import UnsupportedSchemaError + import pyblish.api from ayon_core.pipeline import PublishValidationError @@ -36,7 +38,16 @@ class ValidateEditorialPackage(pyblish.api.InstancePlugin): resource_file_names = {os.path.basename(path) for path in resource_paths} - otio_data = opentimelineio.adapters.read_from_file(otio_path) + try: + otio_data = opentimelineio.adapters.read_from_file(otio_path) + except UnsupportedSchemaError as e: + raise PublishValidationError( + f"Unsupported schema in otio file '{otio_path}'." + "Version of your OpenTimelineIO library is too old." + "Please update it to the latest version." + f"Current version is '{opentimelineio.__version__}', " + "but required is at least 0.16.0." + ) from e target_urls = self._get_all_target_urls(otio_data) missing_files = set() From 2e120020fafcb3e9bb76f0705f6f240b9663291c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 May 2024 18:55:39 +0800 Subject: [PATCH 35/65] use ayon_substancepainter as import --- .../ayon_substancepainter/plugins/create/create_workfile.py | 2 +- .../ayon_substancepainter/plugins/publish/extract_textures.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py index b100e4189d..0bf451f0e2 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py @@ -16,7 +16,7 @@ import substance_painter.project class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" - identifier = "io.openpype.creators.substancepainter.workfile" + identifier = "io.ayon.creators.substancepainter.workfile" label = "Workfile" product_type = "workfile" icon = "document" diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py index 361ad788ca..52212922ae 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/publish/extract_textures.py @@ -1,6 +1,6 @@ import substance_painter.export from ayon_core.pipeline import KnownPublishError, publish -from ayon_core.hosts.substancepainter.api.lib import set_layer_stack_opacity +from ayon_substancepainter.api.lib import set_layer_stack_opacity class ExtractTextures(publish.Extractor, From 1e0d7e5db0ae5d351bb5870f742257b090cca390 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 May 2024 20:44:48 +0800 Subject: [PATCH 36/65] move max hosts to ayon_max --- .../hosts/max => server_addon/max/client/ayon_max}/__init__.py | 0 .../hosts/max => server_addon/max/client/ayon_max}/addon.py | 0 .../max => server_addon/max/client/ayon_max}/api/__init__.py | 0 .../hosts/max => server_addon/max/client/ayon_max}/api/action.py | 0 .../max => server_addon/max/client/ayon_max}/api/colorspace.py | 0 .../hosts/max => server_addon/max/client/ayon_max}/api/lib.py | 0 .../max/client/ayon_max}/api/lib_renderproducts.py | 0 .../max/client/ayon_max}/api/lib_rendersettings.py | 0 .../hosts/max => server_addon/max/client/ayon_max}/api/menu.py | 0 .../max => server_addon/max/client/ayon_max}/api/pipeline.py | 0 .../hosts/max => server_addon/max/client/ayon_max}/api/plugin.py | 0 .../max/client/ayon_max}/api/preview_animation.py | 0 .../max/client/ayon_max}/hooks/force_startup_script.py | 0 .../max/client/ayon_max}/hooks/inject_python.py | 0 .../max => server_addon/max/client/ayon_max}/hooks/set_paths.py | 0 .../max => server_addon/max/client/ayon_max}/plugins/__init__.py | 0 .../max/client/ayon_max}/plugins/create/create_camera.py | 0 .../max/client/ayon_max}/plugins/create/create_maxScene.py | 0 .../max/client/ayon_max}/plugins/create/create_model.py | 0 .../max/client/ayon_max}/plugins/create/create_pointcache.py | 0 .../max/client/ayon_max}/plugins/create/create_pointcloud.py | 0 .../max/client/ayon_max}/plugins/create/create_redshift_proxy.py | 0 .../max/client/ayon_max}/plugins/create/create_render.py | 0 .../max/client/ayon_max}/plugins/create/create_review.py | 0 .../max/client/ayon_max}/plugins/create/create_tycache.py | 0 .../max/client/ayon_max}/plugins/create/create_workfile.py | 0 .../max/client/ayon_max}/plugins/load/load_camera_fbx.py | 0 .../max/client/ayon_max}/plugins/load/load_max_scene.py | 0 .../max/client/ayon_max}/plugins/load/load_model.py | 0 .../max/client/ayon_max}/plugins/load/load_model_fbx.py | 0 .../max/client/ayon_max}/plugins/load/load_model_obj.py | 0 .../max/client/ayon_max}/plugins/load/load_model_usd.py | 0 .../max/client/ayon_max}/plugins/load/load_pointcache.py | 0 .../max/client/ayon_max}/plugins/load/load_pointcache_ornatrix.py | 0 .../max/client/ayon_max}/plugins/load/load_pointcloud.py | 0 .../max/client/ayon_max}/plugins/load/load_redshift_proxy.py | 0 .../max/client/ayon_max}/plugins/load/load_tycache.py | 0 .../max/client/ayon_max}/plugins/publish/collect_current_file.py | 0 .../max/client/ayon_max}/plugins/publish/collect_frame_range.py | 0 .../max/client/ayon_max}/plugins/publish/collect_members.py | 0 .../max/client/ayon_max}/plugins/publish/collect_render.py | 0 .../max/client/ayon_max}/plugins/publish/collect_review.py | 0 .../ayon_max}/plugins/publish/collect_tycache_attributes.py | 0 .../max/client/ayon_max}/plugins/publish/collect_workfile.py | 0 .../max/client/ayon_max}/plugins/publish/extract_alembic.py | 0 .../max/client/ayon_max}/plugins/publish/extract_fbx.py | 0 .../max/client/ayon_max}/plugins/publish/extract_max_scene_raw.py | 0 .../max/client/ayon_max}/plugins/publish/extract_model_obj.py | 0 .../max/client/ayon_max}/plugins/publish/extract_model_usd.py | 0 .../max/client/ayon_max}/plugins/publish/extract_pointcloud.py | 0 .../client/ayon_max}/plugins/publish/extract_redshift_proxy.py | 0 .../client/ayon_max}/plugins/publish/extract_review_animation.py | 0 .../max/client/ayon_max}/plugins/publish/extract_thumbnail.py | 0 .../max/client/ayon_max}/plugins/publish/extract_tycache.py | 0 .../client/ayon_max}/plugins/publish/help/validate_model_name.xml | 0 .../ayon_max}/plugins/publish/increment_workfile_version.py | 0 .../max/client/ayon_max}/plugins/publish/save_scene.py | 0 .../client/ayon_max}/plugins/publish/save_scenes_for_cameras.py | 0 .../max/client/ayon_max}/plugins/publish/validate_attributes.py | 0 .../ayon_max}/plugins/publish/validate_camera_attributes.py | 0 .../client/ayon_max}/plugins/publish/validate_camera_contents.py | 0 .../ayon_max}/plugins/publish/validate_extended_viewport.py | 0 .../max/client/ayon_max}/plugins/publish/validate_frame_range.py | 0 .../ayon_max}/plugins/publish/validate_instance_has_members.py | 0 .../ayon_max}/plugins/publish/validate_instance_in_context.py | 0 .../client/ayon_max}/plugins/publish/validate_loaded_plugin.py | 0 .../max/client/ayon_max}/plugins/publish/validate_mesh_has_uv.py | 0 .../client/ayon_max}/plugins/publish/validate_model_contents.py | 0 .../max/client/ayon_max}/plugins/publish/validate_model_name.py | 0 .../max/client/ayon_max}/plugins/publish/validate_no_animation.py | 0 .../max/client/ayon_max}/plugins/publish/validate_pointcloud.py | 0 .../ayon_max}/plugins/publish/validate_renderable_camera.py | 0 .../ayon_max}/plugins/publish/validate_renderer_redshift_proxy.py | 0 .../max/client/ayon_max}/plugins/publish/validate_renderpasses.py | 0 .../ayon_max}/plugins/publish/validate_resolution_setting.py | 0 .../max/client/ayon_max}/plugins/publish/validate_scene_saved.py | 0 .../max/client/ayon_max}/plugins/publish/validate_tyflow_data.py | 0 .../max => server_addon/max/client/ayon_max}/startup/startup.ms | 0 .../max => server_addon/max/client/ayon_max}/startup/startup.py | 0 79 files changed, 0 insertions(+), 0 deletions(-) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/__init__.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/addon.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/__init__.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/action.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/colorspace.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/lib.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/lib_renderproducts.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/lib_rendersettings.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/menu.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/pipeline.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/plugin.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/api/preview_animation.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/hooks/force_startup_script.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/hooks/inject_python.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/hooks/set_paths.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/__init__.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_camera.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_maxScene.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_model.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_pointcache.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_pointcloud.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_redshift_proxy.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_render.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_review.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_tycache.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/create/create_workfile.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_camera_fbx.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_max_scene.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_model.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_model_fbx.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_model_obj.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_model_usd.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_pointcache.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_pointcache_ornatrix.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_pointcloud.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_redshift_proxy.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/load/load_tycache.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_current_file.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_frame_range.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_members.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_render.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_review.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_tycache_attributes.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/collect_workfile.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_alembic.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_fbx.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_max_scene_raw.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_model_obj.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_model_usd.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_pointcloud.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_redshift_proxy.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_review_animation.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_thumbnail.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/extract_tycache.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/help/validate_model_name.xml (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/increment_workfile_version.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/save_scene.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/save_scenes_for_cameras.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_attributes.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_camera_attributes.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_camera_contents.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_extended_viewport.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_frame_range.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_instance_has_members.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_instance_in_context.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_loaded_plugin.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_mesh_has_uv.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_model_contents.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_model_name.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_no_animation.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_pointcloud.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_renderable_camera.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_renderer_redshift_proxy.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_renderpasses.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_resolution_setting.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_scene_saved.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/plugins/publish/validate_tyflow_data.py (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/startup/startup.ms (100%) rename {client/ayon_core/hosts/max => server_addon/max/client/ayon_max}/startup/startup.py (100%) diff --git a/client/ayon_core/hosts/max/__init__.py b/server_addon/max/client/ayon_max/__init__.py similarity index 100% rename from client/ayon_core/hosts/max/__init__.py rename to server_addon/max/client/ayon_max/__init__.py diff --git a/client/ayon_core/hosts/max/addon.py b/server_addon/max/client/ayon_max/addon.py similarity index 100% rename from client/ayon_core/hosts/max/addon.py rename to server_addon/max/client/ayon_max/addon.py diff --git a/client/ayon_core/hosts/max/api/__init__.py b/server_addon/max/client/ayon_max/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/max/api/__init__.py rename to server_addon/max/client/ayon_max/api/__init__.py diff --git a/client/ayon_core/hosts/max/api/action.py b/server_addon/max/client/ayon_max/api/action.py similarity index 100% rename from client/ayon_core/hosts/max/api/action.py rename to server_addon/max/client/ayon_max/api/action.py diff --git a/client/ayon_core/hosts/max/api/colorspace.py b/server_addon/max/client/ayon_max/api/colorspace.py similarity index 100% rename from client/ayon_core/hosts/max/api/colorspace.py rename to server_addon/max/client/ayon_max/api/colorspace.py diff --git a/client/ayon_core/hosts/max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py similarity index 100% rename from client/ayon_core/hosts/max/api/lib.py rename to server_addon/max/client/ayon_max/api/lib.py diff --git a/client/ayon_core/hosts/max/api/lib_renderproducts.py b/server_addon/max/client/ayon_max/api/lib_renderproducts.py similarity index 100% rename from client/ayon_core/hosts/max/api/lib_renderproducts.py rename to server_addon/max/client/ayon_max/api/lib_renderproducts.py diff --git a/client/ayon_core/hosts/max/api/lib_rendersettings.py b/server_addon/max/client/ayon_max/api/lib_rendersettings.py similarity index 100% rename from client/ayon_core/hosts/max/api/lib_rendersettings.py rename to server_addon/max/client/ayon_max/api/lib_rendersettings.py diff --git a/client/ayon_core/hosts/max/api/menu.py b/server_addon/max/client/ayon_max/api/menu.py similarity index 100% rename from client/ayon_core/hosts/max/api/menu.py rename to server_addon/max/client/ayon_max/api/menu.py diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/server_addon/max/client/ayon_max/api/pipeline.py similarity index 100% rename from client/ayon_core/hosts/max/api/pipeline.py rename to server_addon/max/client/ayon_max/api/pipeline.py diff --git a/client/ayon_core/hosts/max/api/plugin.py b/server_addon/max/client/ayon_max/api/plugin.py similarity index 100% rename from client/ayon_core/hosts/max/api/plugin.py rename to server_addon/max/client/ayon_max/api/plugin.py diff --git a/client/ayon_core/hosts/max/api/preview_animation.py b/server_addon/max/client/ayon_max/api/preview_animation.py similarity index 100% rename from client/ayon_core/hosts/max/api/preview_animation.py rename to server_addon/max/client/ayon_max/api/preview_animation.py diff --git a/client/ayon_core/hosts/max/hooks/force_startup_script.py b/server_addon/max/client/ayon_max/hooks/force_startup_script.py similarity index 100% rename from client/ayon_core/hosts/max/hooks/force_startup_script.py rename to server_addon/max/client/ayon_max/hooks/force_startup_script.py diff --git a/client/ayon_core/hosts/max/hooks/inject_python.py b/server_addon/max/client/ayon_max/hooks/inject_python.py similarity index 100% rename from client/ayon_core/hosts/max/hooks/inject_python.py rename to server_addon/max/client/ayon_max/hooks/inject_python.py diff --git a/client/ayon_core/hosts/max/hooks/set_paths.py b/server_addon/max/client/ayon_max/hooks/set_paths.py similarity index 100% rename from client/ayon_core/hosts/max/hooks/set_paths.py rename to server_addon/max/client/ayon_max/hooks/set_paths.py diff --git a/client/ayon_core/hosts/max/plugins/__init__.py b/server_addon/max/client/ayon_max/plugins/__init__.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/__init__.py rename to server_addon/max/client/ayon_max/plugins/__init__.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_camera.py b/server_addon/max/client/ayon_max/plugins/create/create_camera.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_camera.py rename to server_addon/max/client/ayon_max/plugins/create/create_camera.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_maxScene.py b/server_addon/max/client/ayon_max/plugins/create/create_maxScene.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_maxScene.py rename to server_addon/max/client/ayon_max/plugins/create/create_maxScene.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_model.py b/server_addon/max/client/ayon_max/plugins/create/create_model.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_model.py rename to server_addon/max/client/ayon_max/plugins/create/create_model.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_pointcache.py b/server_addon/max/client/ayon_max/plugins/create/create_pointcache.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_pointcache.py rename to server_addon/max/client/ayon_max/plugins/create/create_pointcache.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_pointcloud.py b/server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_pointcloud.py rename to server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_redshift_proxy.py rename to server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_render.py b/server_addon/max/client/ayon_max/plugins/create/create_render.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_render.py rename to server_addon/max/client/ayon_max/plugins/create/create_render.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_review.py b/server_addon/max/client/ayon_max/plugins/create/create_review.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_review.py rename to server_addon/max/client/ayon_max/plugins/create/create_review.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_tycache.py b/server_addon/max/client/ayon_max/plugins/create/create_tycache.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_tycache.py rename to server_addon/max/client/ayon_max/plugins/create/create_tycache.py diff --git a/client/ayon_core/hosts/max/plugins/create/create_workfile.py b/server_addon/max/client/ayon_max/plugins/create/create_workfile.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/create/create_workfile.py rename to server_addon/max/client/ayon_max/plugins/create/create_workfile.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py b/server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py rename to server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_max_scene.py b/server_addon/max/client/ayon_max/plugins/load/load_max_scene.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_max_scene.py rename to server_addon/max/client/ayon_max/plugins/load/load_max_scene.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_model.py b/server_addon/max/client/ayon_max/plugins/load/load_model.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_model.py rename to server_addon/max/client/ayon_max/plugins/load/load_model.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py b/server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_model_fbx.py rename to server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py b/server_addon/max/client/ayon_max/plugins/load/load_model_obj.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_model_obj.py rename to server_addon/max/client/ayon_max/plugins/load/load_model_obj.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_usd.py b/server_addon/max/client/ayon_max/plugins/load/load_model_usd.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_model_usd.py rename to server_addon/max/client/ayon_max/plugins/load/load_model_usd.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_pointcache.py rename to server_addon/max/client/ayon_max/plugins/load/load_pointcache.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py rename to server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_pointcloud.py rename to server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py rename to server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py diff --git a/client/ayon_core/hosts/max/plugins/load/load_tycache.py b/server_addon/max/client/ayon_max/plugins/load/load_tycache.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/load/load_tycache.py rename to server_addon/max/client/ayon_max/plugins/load/load_tycache.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_current_file.py b/server_addon/max/client/ayon_max/plugins/publish/collect_current_file.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_current_file.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_current_file.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_frame_range.py b/server_addon/max/client/ayon_max/plugins/publish/collect_frame_range.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_frame_range.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_frame_range.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_members.py b/server_addon/max/client/ayon_max/plugins/publish/collect_members.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_members.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_members.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_render.py b/server_addon/max/client/ayon_max/plugins/publish/collect_render.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_render.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_render.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_review.py b/server_addon/max/client/ayon_max/plugins/publish/collect_review.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_review.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_review.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_tycache_attributes.py b/server_addon/max/client/ayon_max/plugins/publish/collect_tycache_attributes.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_tycache_attributes.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_tycache_attributes.py diff --git a/client/ayon_core/hosts/max/plugins/publish/collect_workfile.py b/server_addon/max/client/ayon_max/plugins/publish/collect_workfile.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/collect_workfile.py rename to server_addon/max/client/ayon_max/plugins/publish/collect_workfile.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_alembic.py b/server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_alembic.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_fbx.py b/server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_fbx.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_max_scene_raw.py b/server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_max_scene_raw.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_model_obj.py b/server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_model_obj.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_model_usd.py b/server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_model_usd.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_pointcloud.py b/server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_pointcloud.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_redshift_proxy.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_review_animation.py b/server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_review_animation.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_thumbnail.py b/server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_thumbnail.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_tycache.py b/server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/extract_tycache.py rename to server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py diff --git a/client/ayon_core/hosts/max/plugins/publish/help/validate_model_name.xml b/server_addon/max/client/ayon_max/plugins/publish/help/validate_model_name.xml similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/help/validate_model_name.xml rename to server_addon/max/client/ayon_max/plugins/publish/help/validate_model_name.xml diff --git a/client/ayon_core/hosts/max/plugins/publish/increment_workfile_version.py b/server_addon/max/client/ayon_max/plugins/publish/increment_workfile_version.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/increment_workfile_version.py rename to server_addon/max/client/ayon_max/plugins/publish/increment_workfile_version.py diff --git a/client/ayon_core/hosts/max/plugins/publish/save_scene.py b/server_addon/max/client/ayon_max/plugins/publish/save_scene.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/save_scene.py rename to server_addon/max/client/ayon_max/plugins/publish/save_scene.py diff --git a/client/ayon_core/hosts/max/plugins/publish/save_scenes_for_cameras.py b/server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/save_scenes_for_cameras.py rename to server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_attributes.py b/server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_attributes.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_camera_attributes.py b/server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_camera_attributes.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py b/server_addon/max/client/ayon_max/plugins/publish/validate_camera_contents.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_camera_contents.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_extended_viewport.py b/server_addon/max/client/ayon_max/plugins/publish/validate_extended_viewport.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_extended_viewport.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_extended_viewport.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py b/server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_instance_has_members.py b/server_addon/max/client/ayon_max/plugins/publish/validate_instance_has_members.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_instance_has_members.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_instance_has_members.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_instance_in_context.py b/server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_instance_in_context.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_loaded_plugin.py b/server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_loaded_plugin.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py b/server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_model_contents.py b/server_addon/max/client/ayon_max/plugins/publish/validate_model_contents.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_model_contents.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_model_contents.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_model_name.py b/server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_model_name.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_no_animation.py b/server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_no_animation.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_pointcloud.py b/server_addon/max/client/ayon_max/plugins/publish/validate_pointcloud.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_pointcloud.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_pointcloud.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_renderable_camera.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_renderable_camera.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py b/server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_scene_saved.py b/server_addon/max/client/ayon_max/plugins/publish/validate_scene_saved.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_scene_saved.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_scene_saved.py diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_tyflow_data.py b/server_addon/max/client/ayon_max/plugins/publish/validate_tyflow_data.py similarity index 100% rename from client/ayon_core/hosts/max/plugins/publish/validate_tyflow_data.py rename to server_addon/max/client/ayon_max/plugins/publish/validate_tyflow_data.py diff --git a/client/ayon_core/hosts/max/startup/startup.ms b/server_addon/max/client/ayon_max/startup/startup.ms similarity index 100% rename from client/ayon_core/hosts/max/startup/startup.ms rename to server_addon/max/client/ayon_max/startup/startup.ms diff --git a/client/ayon_core/hosts/max/startup/startup.py b/server_addon/max/client/ayon_max/startup/startup.py similarity index 100% rename from client/ayon_core/hosts/max/startup/startup.py rename to server_addon/max/client/ayon_max/startup/startup.py From 829a3e93def9d4041e2c39d6ccc25be3e1f7e368 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 May 2024 22:34:37 +0800 Subject: [PATCH 37/65] add setting category to ayon_max host --- .../deadline/plugins/publish/submit_max_deadline.py | 8 ++++---- server_addon/max/client/ayon_max/api/lib.py | 2 +- .../max/client/ayon_max/api/lib_renderproducts.py | 2 +- .../max/client/ayon_max/api/lib_rendersettings.py | 2 +- server_addon/max/client/ayon_max/api/menu.py | 2 +- server_addon/max/client/ayon_max/api/pipeline.py | 10 +++++----- .../max/client/ayon_max/api/preview_animation.py | 2 +- .../max/client/ayon_max/hooks/force_startup_script.py | 2 +- .../client/ayon_max/plugins/create/create_camera.py | 4 +++- .../client/ayon_max/plugins/create/create_maxScene.py | 4 +++- .../max/client/ayon_max/plugins/create/create_model.py | 4 +++- .../ayon_max/plugins/create/create_pointcache.py | 4 +++- .../ayon_max/plugins/create/create_pointcloud.py | 4 +++- .../ayon_max/plugins/create/create_redshift_proxy.py | 4 +++- .../client/ayon_max/plugins/create/create_render.py | 6 ++++-- .../client/ayon_max/plugins/create/create_review.py | 4 +++- .../client/ayon_max/plugins/create/create_tycache.py | 4 +++- .../client/ayon_max/plugins/create/create_workfile.py | 6 ++++-- .../client/ayon_max/plugins/load/load_camera_fbx.py | 6 +++--- .../max/client/ayon_max/plugins/load/load_max_scene.py | 6 +++--- .../max/client/ayon_max/plugins/load/load_model.py | 6 +++--- .../max/client/ayon_max/plugins/load/load_model_fbx.py | 8 ++++---- .../max/client/ayon_max/plugins/load/load_model_obj.py | 6 +++--- .../max/client/ayon_max/plugins/load/load_model_usd.py | 8 ++++---- .../client/ayon_max/plugins/load/load_pointcache.py | 6 +++--- .../ayon_max/plugins/load/load_pointcache_ornatrix.py | 6 +++--- .../client/ayon_max/plugins/load/load_pointcloud.py | 6 +++--- .../ayon_max/plugins/load/load_redshift_proxy.py | 6 +++--- .../max/client/ayon_max/plugins/load/load_tycache.py | 6 +++--- .../client/ayon_max/plugins/publish/collect_render.py | 8 ++++---- .../client/ayon_max/plugins/publish/collect_review.py | 2 +- .../client/ayon_max/plugins/publish/extract_alembic.py | 4 ++-- .../max/client/ayon_max/plugins/publish/extract_fbx.py | 4 ++-- .../ayon_max/plugins/publish/extract_max_scene_raw.py | 2 ++ .../ayon_max/plugins/publish/extract_model_obj.py | 6 ++++-- .../ayon_max/plugins/publish/extract_model_usd.py | 4 +++- .../ayon_max/plugins/publish/extract_pointcloud.py | 2 +- .../ayon_max/plugins/publish/extract_redshift_proxy.py | 2 +- .../plugins/publish/extract_review_animation.py | 2 +- .../ayon_max/plugins/publish/extract_thumbnail.py | 2 +- .../client/ayon_max/plugins/publish/extract_tycache.py | 2 +- .../plugins/publish/save_scenes_for_cameras.py | 4 ++-- .../ayon_max/plugins/publish/validate_attributes.py | 2 ++ .../plugins/publish/validate_camera_attributes.py | 4 +++- .../ayon_max/plugins/publish/validate_frame_range.py | 4 +++- .../plugins/publish/validate_instance_in_context.py | 4 +++- .../ayon_max/plugins/publish/validate_loaded_plugin.py | 4 +++- .../ayon_max/plugins/publish/validate_mesh_has_uv.py | 4 +++- .../ayon_max/plugins/publish/validate_model_name.py | 5 ++++- .../ayon_max/plugins/publish/validate_no_animation.py | 4 +++- .../plugins/publish/validate_renderable_camera.py | 2 +- .../publish/validate_renderer_redshift_proxy.py | 2 +- .../ayon_max/plugins/publish/validate_renderpasses.py | 4 +++- .../plugins/publish/validate_resolution_setting.py | 2 +- server_addon/max/client/ayon_max/startup/startup.py | 2 +- server_addon/max/package.py | 8 +++++++- 56 files changed, 145 insertions(+), 94 deletions(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py index ababb01285..4cb510f1cb 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py @@ -15,11 +15,11 @@ from ayon_core.pipeline.publish.lib import ( replace_with_published_scene_path ) from ayon_core.pipeline.publish import KnownPublishError -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api.lib import ( get_current_renderer, get_multipass_setting ) -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings +from ayon_max.api.lib_rendersettings import RenderSettings from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo @@ -205,11 +205,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def _use_published_name(self, data, project_settings): # Not all hosts can import these modules. - from ayon_core.hosts.max.api.lib import ( + from ayon_max.api.lib import ( get_current_renderer, get_multipass_setting ) - from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings + from ayon_max.api.lib_rendersettings import RenderSettings instance = self._instance job_info = copy.deepcopy(self.job_info) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index f20f754248..eb22dbafd2 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -20,7 +20,7 @@ from pymxs import runtime as rt JSON_PREFIX = "JSON::" -log = logging.getLogger("ayon_core.hosts.max") +log = logging.getLogger("ayon_max") def get_main_window(): diff --git a/server_addon/max/client/ayon_max/api/lib_renderproducts.py b/server_addon/max/client/ayon_max/api/lib_renderproducts.py index 710ed0031a..82a6a0c20c 100644 --- a/server_addon/max/client/ayon_max/api/lib_renderproducts.py +++ b/server_addon/max/client/ayon_max/api/lib_renderproducts.py @@ -6,7 +6,7 @@ import os from pymxs import runtime as rt -from ayon_core.hosts.max.api.lib import get_current_renderer +from ayon_max.api.lib import get_current_renderer from ayon_core.pipeline import get_current_project_name from ayon_core.settings import get_project_settings diff --git a/server_addon/max/client/ayon_max/api/lib_rendersettings.py b/server_addon/max/client/ayon_max/api/lib_rendersettings.py index 35b6d064c1..4b65e1397e 100644 --- a/server_addon/max/client/ayon_max/api/lib_rendersettings.py +++ b/server_addon/max/client/ayon_max/api/lib_rendersettings.py @@ -5,7 +5,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_project_name from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api.lib import ( set_render_frame_range, get_current_renderer, get_default_render_folder diff --git a/server_addon/max/client/ayon_max/api/menu.py b/server_addon/max/client/ayon_max/api/menu.py index c6ceeb3a43..25dd39fd84 100644 --- a/server_addon/max/client/ayon_max/api/menu.py +++ b/server_addon/max/client/ayon_max/api/menu.py @@ -5,7 +5,7 @@ from qtpy import QtWidgets, QtCore from pymxs import runtime as rt from ayon_core.tools.utils import host_tools -from ayon_core.hosts.max.api import lib +from ayon_max.api import lib class AYONMenu(object): diff --git a/server_addon/max/client/ayon_max/api/pipeline.py b/server_addon/max/client/ayon_max/api/pipeline.py index d9cfc3407f..5f5e896e86 100644 --- a/server_addon/max/client/ayon_max/api/pipeline.py +++ b/server_addon/max/client/ayon_max/api/pipeline.py @@ -14,14 +14,14 @@ from ayon_core.pipeline import ( AVALON_CONTAINER_ID, AYON_CONTAINER_ID, ) -from ayon_core.hosts.max.api.menu import AYONMenu -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.plugin import MS_CUSTOM_ATTRIB -from ayon_core.hosts.max import MAX_HOST_DIR +from ayon_max.api.menu import AYONMenu +from ayon_max.api import lib +from ayon_max.api.plugin import MS_CUSTOM_ATTRIB +from ayon_max import MAX_HOST_DIR from pymxs import runtime as rt # noqa -log = logging.getLogger("ayon_core.hosts.max") +log = logging.getLogger("ayon_max") PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") diff --git a/server_addon/max/client/ayon_max/api/preview_animation.py b/server_addon/max/client/ayon_max/api/preview_animation.py index 399d3b6222..acda5360a1 100644 --- a/server_addon/max/client/ayon_max/api/preview_animation.py +++ b/server_addon/max/client/ayon_max/api/preview_animation.py @@ -3,7 +3,7 @@ import contextlib from pymxs import runtime as rt from .lib import get_max_version, render_resolution -log = logging.getLogger("ayon_core.hosts.max") +log = logging.getLogger("ayon_max") @contextlib.contextmanager diff --git a/server_addon/max/client/ayon_max/hooks/force_startup_script.py b/server_addon/max/client/ayon_max/hooks/force_startup_script.py index 417f0049ab..1699ea300a 100644 --- a/server_addon/max/client/ayon_max/hooks/force_startup_script.py +++ b/server_addon/max/client/ayon_max/hooks/force_startup_script.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Pre-launch to force 3ds max startup script.""" import os -from ayon_core.hosts.max import MAX_HOST_DIR +from ayon_max import MAX_HOST_DIR from ayon_applications import PreLaunchHook, LaunchTypes diff --git a/server_addon/max/client/ayon_max/plugins/create/create_camera.py b/server_addon/max/client/ayon_max/plugins/create/create_camera.py index 42f8cb716d..451e178afc 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_camera.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_camera.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreateCamera(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreateCamera(plugin.MaxCreator): label = "Camera" product_type = "camera" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_maxScene.py b/server_addon/max/client/ayon_max/plugins/create/create_maxScene.py index 0e5768b267..ee58ef663d 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_maxScene.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_maxScene.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating raw max scene.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreateMaxScene(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreateMaxScene(plugin.MaxCreator): label = "Max Scene" product_type = "maxScene" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_model.py b/server_addon/max/client/ayon_max/plugins/create/create_model.py index 297c92067e..f48182ecd7 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_model.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_model.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for model.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreateModel(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreateModel(plugin.MaxCreator): label = "Model" product_type = "model" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_pointcache.py b/server_addon/max/client/ayon_max/plugins/create/create_pointcache.py index eb0686a0c0..6d7aabe12c 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_pointcache.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreatePointCache(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreatePointCache(plugin.MaxCreator): label = "Point Cache" product_type = "pointcache" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py b/server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py index 9a58f4e624..52014d77b2 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_pointcloud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating point cloud.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreatePointCloud(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreatePointCloud(plugin.MaxCreator): label = "Point Cloud" product_type = "pointcloud" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py index 17f5349dc1..bcc96c7efe 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_redshift_proxy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreateRedshiftProxy(plugin.MaxCreator): @@ -8,3 +8,5 @@ class CreateRedshiftProxy(plugin.MaxCreator): label = "Redshift Proxy" product_type = "redshiftproxy" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_render.py b/server_addon/max/client/ayon_max/plugins/create/create_render.py index 60fe628a5e..d1e236f3ef 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_render.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_render.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" import os -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin from ayon_core.lib import BoolDef -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings +from ayon_max.api.lib_rendersettings import RenderSettings class CreateRender(plugin.MaxCreator): @@ -13,6 +13,8 @@ class CreateRender(plugin.MaxCreator): product_type = "maxrender" icon = "gear" + settings_category = "max" + def create(self, product_name, instance_data, pre_create_data): from pymxs import runtime as rt file = rt.maxFileName diff --git a/server_addon/max/client/ayon_max/plugins/create/create_review.py b/server_addon/max/client/ayon_max/plugins/create/create_review.py index 0a0ffd2e46..a49490519a 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_review.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_review.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating review in Max.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin from ayon_core.lib import BoolDef, EnumDef, NumberDef @@ -12,6 +12,8 @@ class CreateReview(plugin.MaxCreator): product_type = "review" icon = "video-camera" + settings_category = "max" + review_width = 1920 review_height = 1080 percentSize = 100 diff --git a/server_addon/max/client/ayon_max/plugins/create/create_tycache.py b/server_addon/max/client/ayon_max/plugins/create/create_tycache.py index 2b3893bf13..cbdd94e272 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_tycache.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_tycache.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating TyCache.""" -from ayon_core.hosts.max.api import plugin +from ayon_max.api import plugin class CreateTyCache(plugin.MaxCreator): @@ -9,3 +9,5 @@ class CreateTyCache(plugin.MaxCreator): label = "TyCache" product_type = "tycache" icon = "gear" + + settings_category = "max" diff --git a/server_addon/max/client/ayon_max/plugins/create/create_workfile.py b/server_addon/max/client/ayon_max/plugins/create/create_workfile.py index 901da6254c..35c41f0fcc 100644 --- a/server_addon/max/client/ayon_max/plugins/create/create_workfile.py +++ b/server_addon/max/client/ayon_max/plugins/create/create_workfile.py @@ -3,8 +3,8 @@ import ayon_api from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.hosts.max.api import plugin -from ayon_core.hosts.max.api.lib import read, imprint +from ayon_max.api import plugin +from ayon_max.api.lib import read, imprint from pymxs import runtime as rt @@ -17,6 +17,8 @@ class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): default_variant = "Main" + settings_category = "max" + def create(self): variant = self.default_variant current_instance = next( diff --git a/server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py b/server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py index 6f1e9988c5..81ea15d52a 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_camera_fbx.py @@ -1,12 +1,12 @@ import os -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_namespace, object_transform_set ) -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, diff --git a/server_addon/max/client/ayon_max/plugins/load/load_max_scene.py b/server_addon/max/client/ayon_max/plugins/load/load_max_scene.py index 97b8c6cd52..7fca69b193 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_max_scene.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_max_scene.py @@ -1,14 +1,14 @@ import os from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import EnumDef -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_namespace, object_transform_set, is_headless ) -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, remove_container_data diff --git a/server_addon/max/client/ayon_max/plugins/load/load_model.py b/server_addon/max/client/ayon_max/plugins/load/load_model.py index 1070fce2bd..2a6bc45c18 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_model.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_model.py @@ -1,12 +1,12 @@ import os from ayon_core.pipeline import load, get_representation_path -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, remove_container_data ) -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( maintained_selection, unique_namespace ) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py b/server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py index 6f5de20ae0..2775e1b453 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_model_fbx.py @@ -1,17 +1,17 @@ import os from ayon_core.pipeline import load, get_representation_path -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, remove_container_data ) -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_namespace, object_transform_set ) -from ayon_core.hosts.max.api.lib import maintained_selection +from ayon_max.api.lib import maintained_selection class FbxModelLoader(load.LoaderPlugin): diff --git a/server_addon/max/client/ayon_max/plugins/load/load_model_obj.py b/server_addon/max/client/ayon_max/plugins/load/load_model_obj.py index a9119259df..d38aadb5bc 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_model_obj.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_model_obj.py @@ -1,13 +1,13 @@ import os -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_namespace, maintained_selection, object_transform_set ) -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, diff --git a/server_addon/max/client/ayon_max/plugins/load/load_model_usd.py b/server_addon/max/client/ayon_max/plugins/load/load_model_usd.py index 2ed5d64a18..f4dd41d5db 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_model_usd.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_model_usd.py @@ -2,15 +2,15 @@ import os from pymxs import runtime as rt from ayon_core.pipeline.load import LoadError -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_namespace, object_transform_set, get_plugins ) -from ayon_core.hosts.max.api.lib import maintained_selection -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.lib import maintained_selection +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 0743b3bb34..d7def3d0ba 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -6,9 +6,9 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os from ayon_core.pipeline import load, get_representation_path -from ayon_core.hosts.max.api import lib, maintained_selection -from ayon_core.hosts.max.api.lib import unique_namespace -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api import lib, maintained_selection +from ayon_max.api.lib import unique_namespace +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, remove_container_data diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py index 47690f84e9..bc997951c1 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache_ornatrix.py @@ -1,20 +1,20 @@ import os from ayon_core.pipeline import load, get_representation_path from ayon_core.pipeline.load import LoadError -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, remove_container_data ) -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api.lib import ( unique_namespace, get_namespace, object_transform_set, get_plugins ) -from ayon_core.hosts.max.api import lib +from ayon_max.api import lib from pymxs import runtime as rt diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py index 0e79882fc5..0fb506d5bd 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcloud.py @@ -1,11 +1,11 @@ import os -from ayon_core.hosts.max.api import lib, maintained_selection -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib, maintained_selection +from ayon_max.api.lib import ( unique_namespace, ) -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, diff --git a/server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py index 22d42390d9..3fd84b7538 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_redshift_proxy.py @@ -6,14 +6,14 @@ from ayon_core.pipeline import ( get_representation_path ) from ayon_core.pipeline.load import LoadError -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, update_custom_attribute_data, get_previous_loaded_object, remove_container_data ) -from ayon_core.hosts.max.api import lib -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib +from ayon_max.api.lib import ( unique_namespace, get_plugins ) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_tycache.py b/server_addon/max/client/ayon_max/plugins/load/load_tycache.py index 7a5296d933..e087d5599a 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_tycache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_tycache.py @@ -1,10 +1,10 @@ import os -from ayon_core.hosts.max.api import lib, maintained_selection -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api import lib, maintained_selection +from ayon_max.api.lib import ( unique_namespace, ) -from ayon_core.hosts.max.api.pipeline import ( +from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data, diff --git a/server_addon/max/client/ayon_max/plugins/publish/collect_render.py b/server_addon/max/client/ayon_max/plugins/publish/collect_render.py index 4ad9dfb3a3..a5e8d65df2 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/collect_render.py +++ b/server_addon/max/client/ayon_max/plugins/publish/collect_render.py @@ -5,10 +5,10 @@ import pyblish.api from pymxs import runtime as rt from ayon_core.pipeline.publish import KnownPublishError -from ayon_core.hosts.max.api import colorspace -from ayon_core.hosts.max.api.lib import get_max_version, get_current_renderer -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings -from ayon_core.hosts.max.api.lib_renderproducts import RenderProducts +from ayon_max.api import colorspace +from ayon_max.api.lib import get_max_version, get_current_renderer +from ayon_max.api.lib_rendersettings import RenderSettings +from ayon_max.api.lib_renderproducts import RenderProducts class CollectRender(pyblish.api.InstancePlugin): diff --git a/server_addon/max/client/ayon_max/plugins/publish/collect_review.py b/server_addon/max/client/ayon_max/plugins/publish/collect_review.py index d746e2b2db..321aa7439c 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/collect_review.py +++ b/server_addon/max/client/ayon_max/plugins/publish/collect_review.py @@ -4,7 +4,7 @@ import pyblish.api from pymxs import runtime as rt from ayon_core.lib import BoolDef -from ayon_core.hosts.max.api.lib import get_max_version +from ayon_max.api.lib import get_max_version from ayon_core.pipeline.publish import ( AYONPyblishPluginMixin, KnownPublishError diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py b/server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py index 67cec23ecc..b0999e5a78 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_alembic.py @@ -41,8 +41,8 @@ import os import pyblish.api from ayon_core.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection -from ayon_core.hosts.max.api.lib import suspended_refresh +from ayon_max.api import maintained_selection +from ayon_max.api.lib import suspended_refresh from ayon_core.lib import BoolDef diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py b/server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py index 3d80588c47..bdfc1d0d78 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_fbx.py @@ -2,8 +2,8 @@ import os import pyblish.api from ayon_core.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection -from ayon_core.hosts.max.api.lib import convert_unit_scale +from ayon_max.api import maintained_selection +from ayon_max.api.lib import convert_unit_scale class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py b/server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py index f5c703564c..ecde6d2ce9 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_max_scene_raw.py @@ -15,6 +15,8 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): families = ["camera", "maxScene", "model"] optional = True + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py b/server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py index 03bdde7d5d..6556bd7809 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_model_obj.py @@ -2,8 +2,8 @@ import os import pyblish.api from ayon_core.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection -from ayon_core.hosts.max.api.lib import suspended_refresh +from ayon_max.api import maintained_selection +from ayon_max.api.lib import suspended_refresh from ayon_core.pipeline.publish import KnownPublishError @@ -18,6 +18,8 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): families = ["model"] optional = True + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py b/server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py index 64791e4c7d..a48126c6e5 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_model_usd.py @@ -3,7 +3,7 @@ import os import pyblish.api from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection +from ayon_max.api import maintained_selection from ayon_core.pipeline import OptionalPyblishPluginMixin, publish @@ -17,6 +17,8 @@ class ExtractModelUSD(publish.Extractor, families = ["model"] optional = True + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py b/server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py index 67dde7f0a6..f763325eb9 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_pointcloud.py @@ -3,7 +3,7 @@ import os import pyblish.api from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection +from ayon_max.api import maintained_selection from ayon_core.pipeline import publish diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py index 6a647670bc..dfb3527be1 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_redshift_proxy.py @@ -2,7 +2,7 @@ import os import pyblish.api from ayon_core.pipeline import publish from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection +from ayon_max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py b/server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py index 12f1fbb63b..b6397d404e 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_review_animation.py @@ -1,7 +1,7 @@ import os import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.max.api.preview_animation import ( +from ayon_max.api.preview_animation import ( render_preview_animation ) diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py b/server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py index 5764ce98c4..183e381be2 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_thumbnail.py @@ -1,7 +1,7 @@ import os import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.max.api.preview_animation import render_preview_animation +from ayon_max.api.preview_animation import render_preview_animation class ExtractThumbnail(publish.Extractor): diff --git a/server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py b/server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py index 50bb06a765..576abe32a2 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py +++ b/server_addon/max/client/ayon_max/plugins/publish/extract_tycache.py @@ -3,7 +3,7 @@ import os import pyblish.api from pymxs import runtime as rt -from ayon_core.hosts.max.api import maintained_selection +from ayon_max.api import maintained_selection from ayon_core.pipeline import publish diff --git a/server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py b/server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py index 817db1b28f..a211210550 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/server_addon/max/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -5,8 +5,8 @@ import tempfile from pymxs import runtime as rt from ayon_core.lib import run_subprocess -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings -from ayon_core.hosts.max.api.lib_renderproducts import RenderProducts +from ayon_max.api.lib_rendersettings import RenderSettings +from ayon_max.api.lib_renderproducts import RenderProducts class SaveScenesForCamera(pyblish.api.InstancePlugin): diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py b/server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py index 354539871f..a489533b2c 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_attributes.py @@ -61,6 +61,8 @@ class ValidateAttributes(OptionalPyblishPluginMixin, actions = [RepairContextAction] optional = True + settings_category = "max" + @classmethod def get_invalid(cls, context): attributes = json.loads( diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py b/server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py index 9398cba2b7..63a2ef39a7 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_camera_attributes.py @@ -6,7 +6,7 @@ from ayon_core.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError ) -from ayon_core.hosts.max.api.action import SelectInvalidAction +from ayon_max.api.action import SelectInvalidAction class ValidateCameraAttributes(OptionalPyblishPluginMixin, @@ -23,6 +23,8 @@ class ValidateCameraAttributes(OptionalPyblishPluginMixin, actions = [SelectInvalidAction, RepairAction] optional = True + settings_category = "max" + DEFAULTS = ["fov", "nearrange", "farrange", "nearclip", "farclip"] CAM_TYPE = ["Freecamera", "Targetcamera", diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py b/server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py index 11b55232d5..9a9f22dd3e 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_frame_range.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( PublishValidationError, KnownPublishError ) -from ayon_core.hosts.max.api.lib import get_frame_range, set_timeline +from ayon_max.api.lib import get_frame_range, set_timeline class ValidateFrameRange(pyblish.api.InstancePlugin, @@ -36,6 +36,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, optional = True actions = [RepairAction] + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): self.log.debug("Skipping Validate Frame Range...") diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py b/server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py index 5107665235..d5bdfe4eb0 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_instance_in_context.py @@ -7,7 +7,7 @@ from ayon_core.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) -from ayon_core.hosts.max.api.action import SelectInvalidAction +from ayon_max.api.action import SelectInvalidAction from pymxs import runtime as rt @@ -27,6 +27,8 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, hosts = ["max"] actions = [SelectInvalidAction, RepairAction] + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py b/server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py index e278041b6b..1fddc7998d 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_loaded_plugin.py @@ -9,7 +9,7 @@ from ayon_core.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError ) -from ayon_core.hosts.max.api.lib import get_plugins +from ayon_max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, @@ -25,6 +25,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, optional = True actions = [RepairAction] + settings_category = "max" + family_plugins_mapping = [] @classmethod diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py b/server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py index ccd91da2be..31143a60c0 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_mesh_has_uv.py @@ -1,6 +1,6 @@ import pyblish.api -from ayon_core.hosts.max.api.action import SelectInvalidAction +from ayon_max.api.action import SelectInvalidAction from ayon_core.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin, @@ -30,6 +30,8 @@ class ValidateMeshHasUVs(pyblish.api.InstancePlugin, actions = [SelectInvalidAction] optional = True + settings_category = "max" + @classmethod def get_invalid(cls, instance): meshes = [member for member in instance.data["members"] diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py b/server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py index eb86e2e5bd..d691b739b7 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_model_name.py @@ -4,7 +4,7 @@ import re import pyblish.api -from ayon_core.hosts.max.api.action import SelectInvalidAction +from ayon_max.api.action import SelectInvalidAction from ayon_core.pipeline.publish import ( OptionalPyblishPluginMixin, @@ -39,6 +39,9 @@ class ValidateModelName(pyblish.api.InstancePlugin, families = ["model"] label = "Validate Model Name" actions = [SelectInvalidAction] + + settings_category = "max" + # defined by settings regex = r"(.*)_(?P.*)_(GEO)" # cache diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py b/server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py index 4b2a18d606..26384954ca 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_no_animation.py @@ -5,7 +5,7 @@ from ayon_core.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) -from ayon_core.hosts.max.api.action import SelectInvalidAction +from ayon_max.api.action import SelectInvalidAction def get_invalid_keys(obj): @@ -39,6 +39,8 @@ class ValidateNoAnimation(pyblish.api.InstancePlugin, label = "Validate No Animation" actions = [SelectInvalidAction] + settings_category = "max" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py index ffd6b183fe..dc05771e1b 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_renderable_camera.py @@ -4,7 +4,7 @@ from ayon_core.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin) from ayon_core.pipeline.publish import RepairAction -from ayon_core.hosts.max.api.lib import get_current_renderer +from ayon_max.api.lib import get_current_renderer from pymxs import runtime as rt diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py index de3a806c85..66c69bc100 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_renderer_redshift_proxy.py @@ -3,7 +3,7 @@ import pyblish.api from ayon_core.pipeline import PublishValidationError from pymxs import runtime as rt from ayon_core.pipeline.publish import RepairAction -from ayon_core.hosts.max.api.lib import get_current_renderer +from ayon_max.api.lib import get_current_renderer class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py b/server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py index 394d3119c4..d0d47c6340 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_renderpasses.py @@ -7,7 +7,7 @@ from ayon_core.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings +from ayon_max.api.lib_rendersettings import RenderSettings class ValidateRenderPasses(OptionalPyblishPluginMixin, @@ -21,6 +21,8 @@ class ValidateRenderPasses(OptionalPyblishPluginMixin, label = "Validate Render Passes" actions = [RepairAction] + settings_category = "max" + def process(self, instance): invalid = self.get_invalid(instance) if invalid: diff --git a/server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py b/server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py index 5f6cd0a21d..9f7ec17dd9 100644 --- a/server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py +++ b/server_addon/max/client/ayon_max/plugins/publish/validate_resolution_setting.py @@ -7,7 +7,7 @@ from ayon_core.pipeline.publish import ( RepairAction, PublishValidationError ) -from ayon_core.hosts.max.api.lib import ( +from ayon_max.api.lib import ( reset_scene_resolution, imprint ) diff --git a/server_addon/max/client/ayon_max/startup/startup.py b/server_addon/max/client/ayon_max/startup/startup.py index 49a861bad0..298c36f226 100644 --- a/server_addon/max/client/ayon_max/startup/startup.py +++ b/server_addon/max/client/ayon_max/startup/startup.py @@ -8,7 +8,7 @@ for path in os.environ["PYTHONPATH"].split(os.pathsep): if path and path not in sys.path: sys.path.append(path) -from ayon_core.hosts.max.api import MaxHost +from ayon_max.api import MaxHost from ayon_core.pipeline import install_host host = MaxHost() diff --git a/server_addon/max/package.py b/server_addon/max/package.py index fb1f1b3050..ddd4e3b33e 100644 --- a/server_addon/max/package.py +++ b/server_addon/max/package.py @@ -1,3 +1,9 @@ name = "max" title = "Max" -version = "0.1.7" +version = "0.2.0" +client_dir = "ayon_max" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {} From dd61d75998d104082da211f7f727b88cc038331b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 May 2024 22:38:04 +0800 Subject: [PATCH 38/65] cosmetic fix --- server_addon/max/client/ayon_max/startup/startup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server_addon/max/client/ayon_max/startup/startup.py b/server_addon/max/client/ayon_max/startup/startup.py index 298c36f226..1462cc93b7 100644 --- a/server_addon/max/client/ayon_max/startup/startup.py +++ b/server_addon/max/client/ayon_max/startup/startup.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- import os import sys - +from ayon_max.api import MaxHost +from ayon_core.pipeline import install_host # this might happen in some 3dsmax version where PYTHONPATH isn't added # to sys.path automatically for path in os.environ["PYTHONPATH"].split(os.pathsep): if path and path not in sys.path: sys.path.append(path) -from ayon_max.api import MaxHost -from ayon_core.pipeline import install_host - host = MaxHost() install_host(host) From bcdecef227bdf5dba791f1801c73dbc0696d9f97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 May 2024 19:13:20 +0200 Subject: [PATCH 39/65] use updatedBy for author value --- client/ayon_core/tools/workfiles/abstract.py | 4 ++++ client/ayon_core/tools/workfiles/models/workfiles.py | 5 ++++- .../tools/workfiles/widgets/files_widget_workarea.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index b45c643d97..1a5ed77019 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -16,6 +16,7 @@ class WorkfileInfo: creation_time (float): Creation time (timestamp). modification_time (float): Modification time (timestamp). created_by (Union[str, none]): User who created the file. + updated_by (Union[str, none]): User who last updated the file. note (str): Note. """ @@ -28,6 +29,7 @@ class WorkfileInfo: creation_time, modification_time, created_by, + updated_by, note, ): self.folder_id = folder_id @@ -37,6 +39,7 @@ class WorkfileInfo: self.creation_time = creation_time self.modification_time = modification_time self.created_by = created_by + self.updated_by = updated_by self.note = note def to_data(self): @@ -54,6 +57,7 @@ class WorkfileInfo: "creation_time": self.creation_time, "modification_time": self.modification_time, "created_by": self.created_by, + "updated_by": self.updated_by, "note": self.note, } diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index bf30b5dfc2..6ef943910d 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -470,9 +470,11 @@ class WorkfileEntitiesModel: ): note = "" created_by = None + updated_by = None if workfile_info: note = workfile_info["attrib"].get("description") or "" created_by = workfile_info.get("createdBy") + updated_by = workfile_info.get("updatedBy") filestat = os.stat(filepath) return WorkfileInfo( @@ -483,6 +485,7 @@ class WorkfileEntitiesModel: creation_time=filestat.st_ctime, modification_time=filestat.st_mtime, created_by=created_by, + updated_by=updated_by, note=note ) @@ -494,7 +497,7 @@ class WorkfileEntitiesModel: for workfile_info in ayon_api.get_workfiles_info( self._controller.get_current_project_name(), task_ids=[task_id], - fields=["id", "path", "attrib", "createdBy"], + fields=["id", "path", "attrib", "createdBy", "updatedBy"], ): workfile_identifier = self._get_workfile_info_identifier( folder_id, task_id, workfile_info["path"] diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index 7b316adf81..7798755f0e 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -213,7 +213,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): item.setData(file_item.filename, FILENAME_ROLE) item.setData(file_item.filepath, FILEPATH_ROLE) - item.setData(file_item.created_by, AUTHOR_ROLE) + item.setData(file_item.updated_by, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_filename[file_item.filename] = item From e09a3635461ca6d07999d03f7c477744a78f3f49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 May 2024 19:21:38 +0200 Subject: [PATCH 40/65] fix updated by access --- client/ayon_core/tools/workfiles/abstract.py | 2 ++ client/ayon_core/tools/workfiles/models/workfiles.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index 1a5ed77019..9ebfd9feed 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -233,6 +233,7 @@ class FileItem: filename, modified, created_by=None, + updated_by=None, representation_id=None, filepath=None, exists=None @@ -241,6 +242,7 @@ class FileItem: self.dirpath = dirpath self.modified = modified self.created_by = created_by + self.updated_by = updated_by self.representation_id = representation_id self._filepath = filepath self._exists = exists diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 6ef943910d..c93bbb6637 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -199,7 +199,8 @@ class WorkareaModel: workdir, filename, modified, - workfile_info.created_by + workfile_info.created_by, + workfile_info.updated_by, )) return items @@ -675,6 +676,7 @@ class PublishWorkfilesModel: filename, created_at.float_timestamp, author, + None, repre_entity["id"] ) From c90db8b432a6b2da2e37de7bbd2185be5cb28584 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 May 2024 19:22:19 +0200 Subject: [PATCH 41/65] added user info to side panel --- .../tools/workfiles/widgets/side_panel.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 5085f4701e..6bb3d98b40 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -147,13 +147,25 @@ class SidePanelWidget(QtWidgets.QWidget): workfile_info.creation_time) modification_time = datetime.datetime.fromtimestamp( workfile_info.modification_time) + created_lines = [ + creation_time.strftime(datetime_format) + ] + if workfile_info.created_by: + created_lines.insert(0, workfile_info.created_by) + + modified_lines = [ + modification_time.strftime(datetime_format) + ] + if workfile_info.updated_by: + modified_lines.insert(0, workfile_info.updated_by) + lines = ( "Size:", size_value, "Created:", - creation_time.strftime(datetime_format), + "
".join(created_lines), "Modified:", - modification_time.strftime(datetime_format) + "
".join(modified_lines), ) self._orig_note = note self._note_input.setPlainText(note) From 7edfd5f2f183359790e1e38fba1521ebcb17ebf0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 May 2024 19:37:40 +0200 Subject: [PATCH 42/65] implemented users model --- .../ayon_core/tools/common_models/__init__.py | 3 + client/ayon_core/tools/common_models/users.py | 84 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 client/ayon_core/tools/common_models/users.py diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index 8895515b1a..f09edfeab2 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -14,6 +14,7 @@ from .hierarchy import ( ) from .thumbnails import ThumbnailsModel from .selection import HierarchyExpectedSelection +from .users import UsersModel __all__ = ( @@ -32,4 +33,6 @@ __all__ = ( "ThumbnailsModel", "HierarchyExpectedSelection", + + "UsersModel", ) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py new file mode 100644 index 0000000000..f8beb31aa1 --- /dev/null +++ b/client/ayon_core/tools/common_models/users.py @@ -0,0 +1,84 @@ +import ayon_api + +from ayon_core.lib import CacheItem + + +class UserItem: + def __init__( + self, + username, + full_name, + email, + avatar_url, + active, + ): + self.username = username + self.full_name = full_name + self.email = email + self.avatar_url = avatar_url + self.active = active + + @classmethod + def from_entity_data(cls, user_data): + return cls( + user_data["name"], + user_data["attrib"]["fullName"], + user_data["attrib"]["email"], + user_data["attrib"]["avatarUrl"], + user_data["active"], + ) + + +class UsersModel: + def __init__(self, controller): + self._controller = controller + self._users_cache = CacheItem(default_factory=list) + + def get_user_items(self): + """Get user items. + + Returns: + List[UserItem]: List of user items. + + """ + self._invalidate_cache() + return self._users_cache.get_data() + + def get_user_items_by_name(self): + """Get user items by name. + + Implemented as most of cases using this model will need to find + user information by username. + + Returns: + Dict[str, UserItem]: Dictionary of user items by name. + + """ + return { + user_item.username: user_item + for user_item in self.get_user_items() + } + + def get_user_item_by_username(self, username): + """Get user item by username. + + Args: + username (str): Username. + + Returns: + Union[UserItem, None]: User item or None if not found. + + """ + self._invalidate_cache() + for user_item in self.get_user_items(): + if user_item.username == username: + return user_item + return None + + def _invalidate_cache(self): + if self._users_cache.is_valid: + return + self._users_cache.update_data([ + UserItem.from_entity_data(user) + for user in ayon_api.get_users() + ]) From 88b73e0a00a7aa32a6647a4eb26522daf1a0c278 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 May 2024 19:37:57 +0200 Subject: [PATCH 43/65] use user full name if available --- client/ayon_core/tools/workfiles/abstract.py | 10 ++++++++++ client/ayon_core/tools/workfiles/control.py | 14 +++++++++++--- .../workfiles/widgets/files_widget_published.py | 10 +++++++++- .../workfiles/widgets/files_widget_workarea.py | 8 +++++++- .../tools/workfiles/widgets/side_panel.py | 17 +++++++++++++++-- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index 9ebfd9feed..f345e20dca 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -536,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass + @abstractmethod + def get_user_items_by_name(self): + """Get user items available on AYON server. + + Returns: + Dict[str, UserItem]: User items by username. + + """ + pass + # Host information @abstractmethod def get_workfile_extensions(self): diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 3048e6be94..8fa9135bc0 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -19,6 +19,7 @@ from ayon_core.tools.common_models import ( HierarchyModel, HierarchyExpectedSelection, ProjectsModel, + UsersModel, ) from .abstract import ( @@ -161,6 +162,7 @@ class BaseWorkfileController( self._save_is_enabled = True # Expected selected folder and task + self._users_model = self._create_users_model() self._expected_selection = self._create_expected_selection_obj() self._selection_model = self._create_selection_model() self._projects_model = self._create_projects_model() @@ -176,6 +178,12 @@ class BaseWorkfileController( def is_host_valid(self): return self._host_is_valid + def _create_users_model(self): + return UsersModel(self) + + def _create_workfiles_model(self): + return WorkfilesModel(self) + def _create_expected_selection_obj(self): return WorkfilesToolExpectedSelection(self) @@ -188,9 +196,6 @@ class BaseWorkfileController( def _create_hierarchy_model(self): return HierarchyModel(self) - def _create_workfiles_model(self): - return WorkfilesModel(self) - @property def event_system(self): """Inner event system for workfiles tool controller. @@ -272,6 +277,9 @@ class BaseWorkfileController( {"enabled": enabled} ) + def get_user_items_by_name(self): + return self._users_model.get_user_items_by_name() + # Host information def get_workfile_extensions(self): host = self._host diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py index 5f2669bb48..2ce8569a9b 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py @@ -192,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel): self._remove_empty_item() self._remove_missing_context_item() + user_items_by_name = self._controller.get_user_items_by_name() + items_to_remove = set(self._items_by_id.keys()) new_items = [] for file_item in file_items: @@ -212,9 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel): else: flags = QtCore.Qt.NoItemFlags + author = file_item.created_by + user_item = user_items_by_name.get(author) + if user_item is not None and user_item.full_name: + author = user_item.full_name + item.setFlags(flags) + item.setData(file_item.filepath, FILEPATH_ROLE) - item.setData(file_item.created_by, AUTHOR_ROLE) + item.setData(author, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_id[repre_id] = item diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index 7798755f0e..5c102dcdd4 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -193,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): return self._remove_empty_item() self._remove_missing_context_item() + user_items_by_name = self._controller.get_user_items_by_name() items_to_remove = set(self._items_by_filename.keys()) new_items = [] @@ -212,8 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): item.setData(file_item.filename, QtCore.Qt.DisplayRole) item.setData(file_item.filename, FILENAME_ROLE) + updated_by = file_item.updated_by + user_item = user_items_by_name.get(updated_by) + if user_item is not None and user_item.full_name: + updated_by = user_item.full_name + item.setData(file_item.filepath, FILEPATH_ROLE) - item.setData(file_item.updated_by, AUTHOR_ROLE) + item.setData(updated_by, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_filename[file_item.filename] = item diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 6bb3d98b40..53fdf0e0ac 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -147,17 +147,30 @@ class SidePanelWidget(QtWidgets.QWidget): workfile_info.creation_time) modification_time = datetime.datetime.fromtimestamp( workfile_info.modification_time) + + user_items_by_name = self._controller.get_user_items_by_name() + + def convert_username(username): + user_item = user_items_by_name.get(username) + if user_item is not None and user_item.full_name: + return user_item.full_name + return username + created_lines = [ creation_time.strftime(datetime_format) ] if workfile_info.created_by: - created_lines.insert(0, workfile_info.created_by) + created_lines.insert( + 0, convert_username(workfile_info.created_by) + ) modified_lines = [ modification_time.strftime(datetime_format) ] if workfile_info.updated_by: - modified_lines.insert(0, workfile_info.updated_by) + modified_lines.insert( + 0, convert_username(workfile_info.updated_by) + ) lines = ( "Size:", From 68e499b5fb98452c778a670e5f6fb15f4dd0269c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 10:38:12 +0200 Subject: [PATCH 44/65] change sizes of splitter --- client/ayon_core/tools/workfiles/widgets/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 1cfae7ec90..8bcff66f50 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): split_widget.addWidget(tasks_widget) split_widget.addWidget(col_3_widget) split_widget.addWidget(side_panel) - split_widget.setSizes([255, 160, 455, 175]) + split_widget.setSizes([255, 175, 550, 190]) body_layout.addWidget(split_widget) @@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): # Force focus on the open button by default, required for Houdini. self._files_widget.setFocus() - self.resize(1200, 600) + self.resize(1260, 600) def _create_col_1_widget(self, controller, parent): col_widget = QtWidgets.QWidget(parent) From c3c4951a31e0f8b1d1002ed7eb82fef0efa4b706 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:11:46 +0200 Subject: [PATCH 45/65] move client code next to server codebase --- .../celaction/client/ayon_celaction}/__init__.py | 0 .../celaction/client/ayon_celaction}/addon.py | 0 .../ayon_celaction}/hooks/pre_celaction_setup.py | 0 .../client/ayon_celaction}/plugins/__init__.py | 0 .../plugins/publish/collect_celaction_cli_kwargs.py | 0 .../plugins/publish/collect_celaction_instances.py | 0 .../plugins/publish/collect_render_path.py | 0 .../plugins/publish/integrate_version_up.py | 0 .../resources/celaction_template_scene.scn | Bin .../client/ayon_celaction}/scripts/__init__.py | 0 .../client/ayon_celaction}/scripts/publish_cli.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/__init__.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/addon.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/hooks/pre_celaction_setup.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/plugins/__init__.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/plugins/publish/collect_celaction_cli_kwargs.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/plugins/publish/collect_celaction_instances.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/plugins/publish/collect_render_path.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/plugins/publish/integrate_version_up.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/resources/celaction_template_scene.scn (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/scripts/__init__.py (100%) rename {client/ayon_core/hosts/celaction => server_addon/celaction/client/ayon_celaction}/scripts/publish_cli.py (100%) diff --git a/client/ayon_core/hosts/celaction/__init__.py b/server_addon/celaction/client/ayon_celaction/__init__.py similarity index 100% rename from client/ayon_core/hosts/celaction/__init__.py rename to server_addon/celaction/client/ayon_celaction/__init__.py diff --git a/client/ayon_core/hosts/celaction/addon.py b/server_addon/celaction/client/ayon_celaction/addon.py similarity index 100% rename from client/ayon_core/hosts/celaction/addon.py rename to server_addon/celaction/client/ayon_celaction/addon.py diff --git a/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py b/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py similarity index 100% rename from client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py rename to server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py diff --git a/client/ayon_core/hosts/celaction/plugins/__init__.py b/server_addon/celaction/client/ayon_celaction/plugins/__init__.py similarity index 100% rename from client/ayon_core/hosts/celaction/plugins/__init__.py rename to server_addon/celaction/client/ayon_celaction/plugins/__init__.py diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_celaction_cli_kwargs.py similarity index 100% rename from client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py rename to server_addon/celaction/client/ayon_celaction/plugins/publish/collect_celaction_cli_kwargs.py diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_celaction_instances.py similarity index 100% rename from client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py rename to server_addon/celaction/client/ayon_celaction/plugins/publish/collect_celaction_instances.py diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py similarity index 100% rename from client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py rename to server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py diff --git a/client/ayon_core/hosts/celaction/plugins/publish/integrate_version_up.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/integrate_version_up.py similarity index 100% rename from client/ayon_core/hosts/celaction/plugins/publish/integrate_version_up.py rename to server_addon/celaction/client/ayon_celaction/plugins/publish/integrate_version_up.py diff --git a/client/ayon_core/hosts/celaction/resources/celaction_template_scene.scn b/server_addon/celaction/client/ayon_celaction/resources/celaction_template_scene.scn similarity index 100% rename from client/ayon_core/hosts/celaction/resources/celaction_template_scene.scn rename to server_addon/celaction/client/ayon_celaction/resources/celaction_template_scene.scn diff --git a/client/ayon_core/hosts/celaction/scripts/__init__.py b/server_addon/celaction/client/ayon_celaction/scripts/__init__.py similarity index 100% rename from client/ayon_core/hosts/celaction/scripts/__init__.py rename to server_addon/celaction/client/ayon_celaction/scripts/__init__.py diff --git a/client/ayon_core/hosts/celaction/scripts/publish_cli.py b/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py similarity index 100% rename from client/ayon_core/hosts/celaction/scripts/publish_cli.py rename to server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py From fb7dc5b07a6279f3668d9674c9d0344041b87fd3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:12:24 +0200 Subject: [PATCH 46/65] removed unnecessary __init__ file --- server_addon/celaction/client/ayon_celaction/plugins/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 server_addon/celaction/client/ayon_celaction/plugins/__init__.py diff --git a/server_addon/celaction/client/ayon_celaction/plugins/__init__.py b/server_addon/celaction/client/ayon_celaction/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 0af0545f533f2112e8f352a374c81d67c673814f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:13:04 +0200 Subject: [PATCH 47/65] fix imports --- .../client/ayon_celaction/hooks/pre_celaction_setup.py | 2 +- .../celaction/client/ayon_celaction/scripts/publish_cli.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py b/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py index 8350c7b7c8..2b76b30015 100644 --- a/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py +++ b/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py @@ -4,7 +4,7 @@ import winreg import subprocess from ayon_core.lib import get_ayon_launcher_args from ayon_applications import PreLaunchHook, LaunchTypes -from ayon_core.hosts.celaction import CELACTION_ROOT_DIR +from ayon_celaction import CELACTION_ROOT_DIR class CelactionPrelaunchHook(PreLaunchHook): diff --git a/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py b/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py index 92019b8702..9888244d6f 100644 --- a/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py +++ b/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py @@ -4,7 +4,7 @@ import sys import pyblish.api import pyblish.util -import ayon_core.hosts.celaction +from ayon_celaction import CELACTION_ROOT_DIR from ayon_core.lib import Logger from ayon_core.tools.utils import host_tools from ayon_core.pipeline import install_ayon_plugins @@ -13,8 +13,7 @@ from ayon_core.pipeline import install_ayon_plugins log = Logger.get_logger("celaction") PUBLISH_HOST = "celaction" -HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.celaction.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(CELACTION_ROOT_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") From 7bd81a83e649b3e02a11af592d0642e904355c58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:13:17 +0200 Subject: [PATCH 48/65] added settings category --- .../ayon_celaction/plugins/publish/collect_render_path.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py index 1bb4d54831..36a033ebb0 100644 --- a/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py +++ b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py @@ -10,6 +10,8 @@ class CollectRenderPath(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.495 families = ["render.farm"] + settings_category = "celaction" + # Presets output_extension = "png" anatomy_template_key_render_files = None From 4267252a36f8ba0841e0cfe909dcb7538dd8295d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:13:27 +0200 Subject: [PATCH 49/65] formatting changes --- .../client/ayon_celaction/hooks/pre_celaction_setup.py | 8 +++----- .../ayon_celaction/plugins/publish/collect_render_path.py | 2 +- .../client/ayon_celaction/scripts/publish_cli.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py b/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py index 2b76b30015..52622d43b8 100644 --- a/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py +++ b/server_addon/celaction/client/ayon_celaction/hooks/pre_celaction_setup.py @@ -8,9 +8,7 @@ from ayon_celaction import CELACTION_ROOT_DIR class CelactionPrelaunchHook(PreLaunchHook): - """ - Bootstrap celacion with pype - """ + """Bootstrap celacion with AYON""" app_groups = {"celaction"} platforms = {"windows"} launch_types = {LaunchTypes.local} @@ -39,7 +37,7 @@ class CelactionPrelaunchHook(PreLaunchHook): CELACTION_ROOT_DIR, "scripts", "publish_cli.py" ) subprocess_args = get_ayon_launcher_args("run", path_to_cli) - openpype_executable = subprocess_args.pop(0) + executable = subprocess_args.pop(0) workfile_settings = self.get_workfile_settings() winreg.SetValueEx( @@ -47,7 +45,7 @@ class CelactionPrelaunchHook(PreLaunchHook): "SubmitAppTitle", 0, winreg.REG_SZ, - openpype_executable + executable ) # add required arguments for workfile path diff --git a/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py index 36a033ebb0..3bcd1c69b3 100644 --- a/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py +++ b/server_addon/celaction/client/ayon_celaction/plugins/publish/collect_render_path.py @@ -1,6 +1,6 @@ import os -import pyblish.api import copy +import pyblish.api class CollectRenderPath(pyblish.api.InstancePlugin): diff --git a/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py b/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py index 9888244d6f..4e54aa253a 100644 --- a/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py +++ b/server_addon/celaction/client/ayon_celaction/scripts/publish_cli.py @@ -18,7 +18,7 @@ PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") def main(): - # Registers pype's Global pyblish plugins + # Registers global pyblish plugins install_ayon_plugins() if os.path.exists(PUBLISH_PATH): From 438230bf5cdcaccf5506eebee1bac8fec917825b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:13:33 +0200 Subject: [PATCH 50/65] update package.py --- server_addon/celaction/package.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server_addon/celaction/package.py b/server_addon/celaction/package.py index 2b11a8630f..8b9069d019 100644 --- a/server_addon/celaction/package.py +++ b/server_addon/celaction/package.py @@ -1,3 +1,12 @@ name = "celaction" title = "CelAction" -version = "0.1.0" +version = "0.2.0" + +client_dir = "ayon_celaction" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = { + "applications": ">=0.2.0", +} From 04126feff2919110755258aa141821ef625a7825 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:15:26 +0200 Subject: [PATCH 51/65] added milestone version --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 939fab68b8..865c6a56a3 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -51,6 +51,7 @@ IGNORED_MODULES_IN_AYON = set() # - this is used to log the missing addon MOVED_ADDON_MILESTONE_VERSIONS = { "applications": VersionInfo(0, 2, 0), + "celaction": VersionInfo(0, 2, 0), "clockify": VersionInfo(0, 2, 0), "traypublisher": VersionInfo(0, 2, 0), "tvpaint": VersionInfo(0, 2, 0), From 6bd274df24f556837c7a6dbba17b66a363421920 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:20:52 +0200 Subject: [PATCH 52/65] moved flame integration next to server codebase --- .../flame => server_addon/flame/client/ayon_flame}/__init__.py | 0 .../hosts/flame => server_addon/flame/client/ayon_flame}/addon.py | 0 .../flame/client/ayon_flame}/api/__init__.py | 0 .../flame/client/ayon_flame}/api/batch_utils.py | 0 .../flame/client/ayon_flame}/api/constants.py | 0 .../flame => server_addon/flame/client/ayon_flame}/api/lib.py | 0 .../flame => server_addon/flame/client/ayon_flame}/api/menu.py | 0 .../flame/client/ayon_flame}/api/pipeline.py | 0 .../flame => server_addon/flame/client/ayon_flame}/api/plugin.py | 0 .../flame/client/ayon_flame}/api/render_utils.py | 0 .../flame/client/ayon_flame}/api/scripts/wiretap_com.py | 0 .../flame => server_addon/flame/client/ayon_flame}/api/utils.py | 0 .../flame => server_addon/flame/client/ayon_flame}/api/workio.py | 0 .../flame/client/ayon_flame}/hooks/pre_flame_setup.py | 0 .../flame/client/ayon_flame}/otio/__init__.py | 0 .../flame/client/ayon_flame}/otio/flame_export.py | 0 .../flame => server_addon/flame/client/ayon_flame}/otio/utils.py | 0 .../flame/client/ayon_flame}/plugins/create/create_shot_clip.py | 0 .../flame/client/ayon_flame}/plugins/load/load_clip.py | 0 .../flame/client/ayon_flame}/plugins/load/load_clip_batch.py | 0 .../client/ayon_flame}/plugins/publish/collect_test_selection.py | 0 .../ayon_flame}/plugins/publish/collect_timeline_instances.py | 0 .../client/ayon_flame}/plugins/publish/collect_timeline_otio.py | 0 .../flame/client/ayon_flame}/plugins/publish/extract_otio_file.py | 0 .../ayon_flame}/plugins/publish/extract_subset_resources.py | 0 .../client/ayon_flame}/plugins/publish/integrate_batch_group.py | 0 .../export_preset/openpype_seg_thumbnails_jpg.xml | 0 .../export_preset/openpype_seg_video_h264.xml | 0 .../startup/openpype_babypublisher/modules/__init__.py | 0 .../startup/openpype_babypublisher/modules/app_utils.py | 0 .../startup/openpype_babypublisher/modules/ftrack_lib.py | 0 .../startup/openpype_babypublisher/modules/panel_app.py | 0 .../startup/openpype_babypublisher/modules/uiwidgets.py | 0 .../startup/openpype_babypublisher/openpype_babypublisher.py | 0 .../flame/client/ayon_flame}/startup/openpype_in_flame.py | 0 35 files changed, 0 insertions(+), 0 deletions(-) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/__init__.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/addon.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/__init__.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/batch_utils.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/constants.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/lib.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/menu.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/pipeline.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/plugin.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/render_utils.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/scripts/wiretap_com.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/utils.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/api/workio.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/hooks/pre_flame_setup.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/otio/__init__.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/otio/flame_export.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/otio/utils.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/create/create_shot_clip.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/load/load_clip.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/load/load_clip_batch.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/collect_test_selection.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/collect_timeline_instances.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/collect_timeline_otio.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/extract_otio_file.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/extract_subset_resources.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/plugins/publish/integrate_batch_group.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/modules/__init__.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/modules/app_utils.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/modules/ftrack_lib.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/modules/panel_app.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/modules/uiwidgets.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_babypublisher/openpype_babypublisher.py (100%) rename {client/ayon_core/hosts/flame => server_addon/flame/client/ayon_flame}/startup/openpype_in_flame.py (100%) diff --git a/client/ayon_core/hosts/flame/__init__.py b/server_addon/flame/client/ayon_flame/__init__.py similarity index 100% rename from client/ayon_core/hosts/flame/__init__.py rename to server_addon/flame/client/ayon_flame/__init__.py diff --git a/client/ayon_core/hosts/flame/addon.py b/server_addon/flame/client/ayon_flame/addon.py similarity index 100% rename from client/ayon_core/hosts/flame/addon.py rename to server_addon/flame/client/ayon_flame/addon.py diff --git a/client/ayon_core/hosts/flame/api/__init__.py b/server_addon/flame/client/ayon_flame/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/flame/api/__init__.py rename to server_addon/flame/client/ayon_flame/api/__init__.py diff --git a/client/ayon_core/hosts/flame/api/batch_utils.py b/server_addon/flame/client/ayon_flame/api/batch_utils.py similarity index 100% rename from client/ayon_core/hosts/flame/api/batch_utils.py rename to server_addon/flame/client/ayon_flame/api/batch_utils.py diff --git a/client/ayon_core/hosts/flame/api/constants.py b/server_addon/flame/client/ayon_flame/api/constants.py similarity index 100% rename from client/ayon_core/hosts/flame/api/constants.py rename to server_addon/flame/client/ayon_flame/api/constants.py diff --git a/client/ayon_core/hosts/flame/api/lib.py b/server_addon/flame/client/ayon_flame/api/lib.py similarity index 100% rename from client/ayon_core/hosts/flame/api/lib.py rename to server_addon/flame/client/ayon_flame/api/lib.py diff --git a/client/ayon_core/hosts/flame/api/menu.py b/server_addon/flame/client/ayon_flame/api/menu.py similarity index 100% rename from client/ayon_core/hosts/flame/api/menu.py rename to server_addon/flame/client/ayon_flame/api/menu.py diff --git a/client/ayon_core/hosts/flame/api/pipeline.py b/server_addon/flame/client/ayon_flame/api/pipeline.py similarity index 100% rename from client/ayon_core/hosts/flame/api/pipeline.py rename to server_addon/flame/client/ayon_flame/api/pipeline.py diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/server_addon/flame/client/ayon_flame/api/plugin.py similarity index 100% rename from client/ayon_core/hosts/flame/api/plugin.py rename to server_addon/flame/client/ayon_flame/api/plugin.py diff --git a/client/ayon_core/hosts/flame/api/render_utils.py b/server_addon/flame/client/ayon_flame/api/render_utils.py similarity index 100% rename from client/ayon_core/hosts/flame/api/render_utils.py rename to server_addon/flame/client/ayon_flame/api/render_utils.py diff --git a/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py b/server_addon/flame/client/ayon_flame/api/scripts/wiretap_com.py similarity index 100% rename from client/ayon_core/hosts/flame/api/scripts/wiretap_com.py rename to server_addon/flame/client/ayon_flame/api/scripts/wiretap_com.py diff --git a/client/ayon_core/hosts/flame/api/utils.py b/server_addon/flame/client/ayon_flame/api/utils.py similarity index 100% rename from client/ayon_core/hosts/flame/api/utils.py rename to server_addon/flame/client/ayon_flame/api/utils.py diff --git a/client/ayon_core/hosts/flame/api/workio.py b/server_addon/flame/client/ayon_flame/api/workio.py similarity index 100% rename from client/ayon_core/hosts/flame/api/workio.py rename to server_addon/flame/client/ayon_flame/api/workio.py diff --git a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py b/server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py similarity index 100% rename from client/ayon_core/hosts/flame/hooks/pre_flame_setup.py rename to server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py diff --git a/client/ayon_core/hosts/flame/otio/__init__.py b/server_addon/flame/client/ayon_flame/otio/__init__.py similarity index 100% rename from client/ayon_core/hosts/flame/otio/__init__.py rename to server_addon/flame/client/ayon_flame/otio/__init__.py diff --git a/client/ayon_core/hosts/flame/otio/flame_export.py b/server_addon/flame/client/ayon_flame/otio/flame_export.py similarity index 100% rename from client/ayon_core/hosts/flame/otio/flame_export.py rename to server_addon/flame/client/ayon_flame/otio/flame_export.py diff --git a/client/ayon_core/hosts/flame/otio/utils.py b/server_addon/flame/client/ayon_flame/otio/utils.py similarity index 100% rename from client/ayon_core/hosts/flame/otio/utils.py rename to server_addon/flame/client/ayon_flame/otio/utils.py diff --git a/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py b/server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py rename to server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py diff --git a/client/ayon_core/hosts/flame/plugins/load/load_clip.py b/server_addon/flame/client/ayon_flame/plugins/load/load_clip.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/load/load_clip.py rename to server_addon/flame/client/ayon_flame/plugins/load/load_clip.py diff --git a/client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py b/server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py rename to server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_test_selection.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/collect_test_selection.py rename to server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py rename to server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py rename to server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/extract_otio_file.py b/server_addon/flame/client/ayon_flame/plugins/publish/extract_otio_file.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/extract_otio_file.py rename to server_addon/flame/client/ayon_flame/plugins/publish/extract_otio_file.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/extract_subset_resources.py b/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/extract_subset_resources.py rename to server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py diff --git a/client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py b/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py similarity index 100% rename from client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py rename to server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/__init__.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/__init__.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/__init__.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/__init__.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/app_utils.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/app_utils.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/ftrack_lib.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/ftrack_lib.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/panel_app.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/panel_app.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/uiwidgets.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/modules/uiwidgets.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py b/server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/openpype_babypublisher.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py rename to server_addon/flame/client/ayon_flame/startup/openpype_babypublisher/openpype_babypublisher.py diff --git a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py b/server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py similarity index 100% rename from client/ayon_core/hosts/flame/startup/openpype_in_flame.py rename to server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py From 7ad67f57b907f5589e87591c315fb4cc793a09f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:28:54 +0200 Subject: [PATCH 53/65] renamed 'HOST_DIR to 'FLAME_ADDON_ROOT' --- server_addon/flame/client/ayon_flame/__init__.py | 4 ++-- server_addon/flame/client/ayon_flame/addon.py | 6 +++--- server_addon/flame/client/ayon_flame/api/pipeline.py | 5 ++--- server_addon/flame/client/ayon_flame/api/utils.py | 5 +++-- .../flame/client/ayon_flame/hooks/pre_flame_setup.py | 5 +++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server_addon/flame/client/ayon_flame/__init__.py b/server_addon/flame/client/ayon_flame/__init__.py index b45f107747..c7b4516c95 100644 --- a/server_addon/flame/client/ayon_flame/__init__.py +++ b/server_addon/flame/client/ayon_flame/__init__.py @@ -1,10 +1,10 @@ from .addon import ( - HOST_DIR, + FLAME_ADDON_ROOT, FlameAddon, ) __all__ = ( - "HOST_DIR", + "FLAME_ADDON_ROOT", "FlameAddon", ) diff --git a/server_addon/flame/client/ayon_flame/addon.py b/server_addon/flame/client/ayon_flame/addon.py index f5560cde7a..2a9c57b24c 100644 --- a/server_addon/flame/client/ayon_flame/addon.py +++ b/server_addon/flame/client/ayon_flame/addon.py @@ -1,7 +1,7 @@ import os from ayon_core.addon import AYONAddon, IHostAddon -HOST_DIR = os.path.dirname(os.path.abspath(__file__)) +FLAME_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class FlameAddon(AYONAddon, IHostAddon): @@ -10,7 +10,7 @@ class FlameAddon(AYONAddon, IHostAddon): def add_implementation_envs(self, env, _app): # Add requirements to DL_PYTHON_HOOK_PATH - env["DL_PYTHON_HOOK_PATH"] = os.path.join(HOST_DIR, "startup") + env["DL_PYTHON_HOOK_PATH"] = os.path.join(FLAME_ADDON_ROOT, "startup") env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) # Set default values if are not already set via settings @@ -25,7 +25,7 @@ class FlameAddon(AYONAddon, IHostAddon): if app.host_name != self.host_name: return [] return [ - os.path.join(HOST_DIR, "hooks") + os.path.join(FLAME_ADDON_ROOT, "hooks") ] def get_workfile_extensions(self): diff --git a/server_addon/flame/client/ayon_flame/api/pipeline.py b/server_addon/flame/client/ayon_flame/api/pipeline.py index 4578d7bb4b..73baca87df 100644 --- a/server_addon/flame/client/ayon_flame/api/pipeline.py +++ b/server_addon/flame/client/ayon_flame/api/pipeline.py @@ -13,6 +13,7 @@ from ayon_core.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from ayon_flame import FLAME_ADDON_ROOT from .lib import ( set_segment_data_marker, set_publish_attribute, @@ -20,10 +21,8 @@ from .lib import ( get_current_sequence, reset_segment_selection ) -from .. import HOST_DIR -API_DIR = os.path.join(HOST_DIR, "api") -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(FLAME_ADDON_ROOT, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") diff --git a/server_addon/flame/client/ayon_flame/api/utils.py b/server_addon/flame/client/ayon_flame/api/utils.py index b76dd92ada..03a694c25c 100644 --- a/server_addon/flame/client/ayon_flame/api/utils.py +++ b/server_addon/flame/client/ayon_flame/api/utils.py @@ -5,6 +5,8 @@ Flame utils for syncing scripts import os import shutil from ayon_core.lib import Logger +from ayon_flame import FLAME_ADDON_ROOT + log = Logger.get_logger(__name__) @@ -16,7 +18,6 @@ def _sync_utility_scripts(env=None): `/opt/Autodesk/shared/python`. This will be always synchronizing those folders. """ - from .. import HOST_DIR env = env or os.environ @@ -26,7 +27,7 @@ def _sync_utility_scripts(env=None): flame_shared_dir = "/opt/Autodesk/shared/python" fsd_paths = [os.path.join( - HOST_DIR, + FLAME_ADDON_ROOT, "api", "utility_scripts" )] diff --git a/server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py b/server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py index 77a9435205..e9e9aca3f4 100644 --- a/server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py +++ b/server_addon/flame/client/ayon_flame/hooks/pre_flame_setup.py @@ -10,7 +10,7 @@ from ayon_core.lib import ( run_subprocess, ) from ayon_applications import PreLaunchHook, LaunchTypes -from ayon_core.hosts import flame as opflame +from ayon_flame import FLAME_ADDON_ROOT class FlamePrelaunch(PreLaunchHook): @@ -23,7 +23,8 @@ class FlamePrelaunch(PreLaunchHook): permissions = 0o777 wtc_script_path = os.path.join( - opflame.HOST_DIR, "api", "scripts", "wiretap_com.py") + FLAME_ADDON_ROOT, "api", "scripts", "wiretap_com.py" + ) launch_types = {LaunchTypes.local} def __init__(self, *args, **kwargs): From 9ce468d15577bed0c3c2aa1cb4fd083bfbac4b4f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:29:22 +0200 Subject: [PATCH 54/65] fixed imports --- server_addon/flame/client/ayon_flame/api/menu.py | 2 +- server_addon/flame/client/ayon_flame/otio/flame_export.py | 2 +- .../client/ayon_flame/plugins/create/create_shot_clip.py | 2 +- .../flame/client/ayon_flame/plugins/load/load_clip.py | 2 +- .../flame/client/ayon_flame/plugins/load/load_clip_batch.py | 2 +- .../ayon_flame/plugins/publish/collect_test_selection.py | 4 ++-- .../ayon_flame/plugins/publish/collect_timeline_instances.py | 4 ++-- .../ayon_flame/plugins/publish/collect_timeline_otio.py | 4 ++-- .../ayon_flame/plugins/publish/extract_subset_resources.py | 4 ++-- .../ayon_flame/plugins/publish/integrate_batch_group.py | 2 +- .../flame/client/ayon_flame/startup/openpype_in_flame.py | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server_addon/flame/client/ayon_flame/api/menu.py b/server_addon/flame/client/ayon_flame/api/menu.py index 7e880483f5..83d75d18d3 100644 --- a/server_addon/flame/client/ayon_flame/api/menu.py +++ b/server_addon/flame/client/ayon_flame/api/menu.py @@ -28,7 +28,7 @@ default_flame_export_presets = { def callback_selection(selection, function): - import ayon_core.hosts.flame.api as opfapi + import ayon_flame.api as opfapi opfapi.CTX.selection = selection print("Hook Selection: \n\t{}".format( pformat({ diff --git a/server_addon/flame/client/ayon_flame/otio/flame_export.py b/server_addon/flame/client/ayon_flame/otio/flame_export.py index cb038f9e9a..bebe9be1c1 100644 --- a/server_addon/flame/client/ayon_flame/otio/flame_export.py +++ b/server_addon/flame/client/ayon_flame/otio/flame_export.py @@ -275,7 +275,7 @@ def create_otio_reference(clip_data, fps=None): def create_otio_clip(clip_data): - from ayon_core.hosts.flame.api import MediaInfoFile, TimeEffectMetadata + from ayon_flame.api import MediaInfoFile, TimeEffectMetadata segment = clip_data["PySegment"] diff --git a/server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py b/server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py index 56f5319f21..120c8c559d 100644 --- a/server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py +++ b/server_addon/flame/client/ayon_flame/plugins/create/create_shot_clip.py @@ -1,5 +1,5 @@ from copy import deepcopy -import ayon_core.hosts.flame.api as opfapi +import ayon_flame.api as opfapi class CreateShotClip(opfapi.Creator): diff --git a/server_addon/flame/client/ayon_flame/plugins/load/load_clip.py b/server_addon/flame/client/ayon_flame/plugins/load/load_clip.py index 40ab9c038b..c8ec7b36c9 100644 --- a/server_addon/flame/client/ayon_flame/plugins/load/load_clip.py +++ b/server_addon/flame/client/ayon_flame/plugins/load/load_clip.py @@ -2,7 +2,7 @@ from copy import deepcopy import os import flame from pprint import pformat -import ayon_core.hosts.flame.api as opfapi +import ayon_flame.api as opfapi from ayon_core.lib import StringTemplate from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, diff --git a/server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py b/server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py index 1b23a8b465..0d7a125af7 100644 --- a/server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py +++ b/server_addon/flame/client/ayon_flame/plugins/load/load_clip_batch.py @@ -2,7 +2,7 @@ from copy import deepcopy import os import flame from pprint import pformat -import ayon_core.hosts.flame.api as opfapi +import ayon_flame.api as opfapi from ayon_core.lib import StringTemplate from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py index 7442e7df48..dac2c862e6 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/collect_test_selection.py @@ -1,8 +1,8 @@ import os import pyblish.api import tempfile -import ayon_core.hosts.flame.api as opfapi -from ayon_core.hosts.flame.otio import flame_export as otio_export +import ayon_flame.api as opfapi +from ayon_flame.otio import flame_export as otio_export import opentimelineio as otio from pprint import pformat reload(otio_export) # noqa diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py index ca5475824d..06108a37a7 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py @@ -1,8 +1,8 @@ import re from types import NoneType import pyblish -import ayon_core.hosts.flame.api as opfapi -from ayon_core.hosts.flame.otio import flame_export +import ayon_flame.api as opfapi +from ayon_flame.otio import flame_export from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID from ayon_core.pipeline.editorial import ( is_overlapping_otio_ranges, diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py index 7609ea7879..139ac5b875 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_otio.py @@ -1,7 +1,7 @@ import pyblish.api -import ayon_core.hosts.flame.api as opfapi -from ayon_core.hosts.flame.otio import flame_export +import ayon_flame.api as opfapi +from ayon_flame.otio import flame_export from ayon_core.pipeline.create import get_product_name diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py b/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py index cae08cd76b..51c0b280b7 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py @@ -5,8 +5,8 @@ from copy import deepcopy import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.flame import api as opfapi -from ayon_core.hosts.flame.api import MediaInfoFile +from ayon_flame import api as opfapi +from ayon_flame.api import MediaInfoFile from ayon_core.pipeline.editorial import ( get_media_range_with_retimes ) diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py b/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py index d8669f836d..776c89d38d 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py @@ -3,7 +3,7 @@ import copy from collections import OrderedDict from pprint import pformat import pyblish -import ayon_core.hosts.flame.api as opfapi +import ayon_flame.api as opfapi import ayon_core.pipeline as op_pipeline from ayon_core.pipeline.workfile import get_workdir diff --git a/server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py b/server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py index b9cbf9700b..8f319a88eb 100644 --- a/server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py +++ b/server_addon/flame/client/ayon_flame/startup/openpype_in_flame.py @@ -4,7 +4,7 @@ from qtpy import QtWidgets from pprint import pformat import atexit -import ayon_core.hosts.flame.api as opfapi +import ayon_flame.api as opfapi from ayon_core.pipeline import ( install_host, registered_host, From c349cc7015dafc9e534f379535189d2502eaee7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:29:30 +0200 Subject: [PATCH 55/65] added settings category --- .../ayon_flame/plugins/publish/collect_timeline_instances.py | 2 ++ .../ayon_flame/plugins/publish/extract_subset_resources.py | 2 ++ .../client/ayon_flame/plugins/publish/integrate_batch_group.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py index 06108a37a7..35591f1a0d 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/collect_timeline_instances.py @@ -24,6 +24,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): label = "Collect timeline Instances" hosts = ["flame"] + settings_category = "flame" + audio_track_items = [] # settings diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py b/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py index 51c0b280b7..66c6181ffb 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/extract_subset_resources.py @@ -24,6 +24,8 @@ class ExtractProductResources(publish.Extractor): families = ["clip"] hosts = ["flame"] + settings_category = "flame" + # plugin defaults keep_original_representation = False diff --git a/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py b/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py index 776c89d38d..f77c9e9116 100644 --- a/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py +++ b/server_addon/flame/client/ayon_flame/plugins/publish/integrate_batch_group.py @@ -16,6 +16,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): hosts = ["flame"] families = ["clip"] + settings_category = "flame" + # settings default_loader = "LoadClip" From c5ca23ff0f098410c19a721e4f710ac98ea1ea8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:29:38 +0200 Subject: [PATCH 56/65] removed resolve import --- server_addon/flame/client/ayon_flame/api/pipeline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server_addon/flame/client/ayon_flame/api/pipeline.py b/server_addon/flame/client/ayon_flame/api/pipeline.py index 73baca87df..121b925920 100644 --- a/server_addon/flame/client/ayon_flame/api/pipeline.py +++ b/server_addon/flame/client/ayon_flame/api/pipeline.py @@ -112,10 +112,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) - # from ayon_core.hosts.resolve import ( - # set_publish_attribute - # ) - # # Whether instances should be passthrough based on new value # timeline_item = instance.data["item"] # set_publish_attribute(timeline_item, new_value) From ded6f0b7beb98c58351a6e93b5699b6638b097f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:30:57 +0200 Subject: [PATCH 57/65] added more information to package --- server_addon/flame/package.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server_addon/flame/package.py b/server_addon/flame/package.py index 8c077ed91d..f228e61f8e 100644 --- a/server_addon/flame/package.py +++ b/server_addon/flame/package.py @@ -1,3 +1,10 @@ name = "flame" title = "Flame" -version = "0.1.0" +version = "0.2.0" + +client_dir = "ayon_flame" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {} From 7de260a0f6dc5141fd510beab84943b6eb1d6249 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:33:32 +0200 Subject: [PATCH 58/65] added version.py --- server_addon/flame/client/ayon_flame/version.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 server_addon/flame/client/ayon_flame/version.py diff --git a/server_addon/flame/client/ayon_flame/version.py b/server_addon/flame/client/ayon_flame/version.py new file mode 100644 index 0000000000..0004797e59 --- /dev/null +++ b/server_addon/flame/client/ayon_flame/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring AYON addon 'flame' version.""" +__version__ = "0.2.0" From 8df1cab5896543a6c5645398acd8192b6ef07bc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:33:46 +0200 Subject: [PATCH 59/65] added flame to version milestones --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 939fab68b8..fa008778dd 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -52,6 +52,7 @@ IGNORED_MODULES_IN_AYON = set() MOVED_ADDON_MILESTONE_VERSIONS = { "applications": VersionInfo(0, 2, 0), "clockify": VersionInfo(0, 2, 0), + "flame": VersionInfo(0, 2, 0), "traypublisher": VersionInfo(0, 2, 0), "tvpaint": VersionInfo(0, 2, 0), "nuke": VersionInfo(0, 2, 0), From cfb06c9a9be33b6f472263200a5f9db26bd4a66a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:36:13 +0200 Subject: [PATCH 60/65] added version.py --- server_addon/celaction/client/ayon_celaction/version.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 server_addon/celaction/client/ayon_celaction/version.py diff --git a/server_addon/celaction/client/ayon_celaction/version.py b/server_addon/celaction/client/ayon_celaction/version.py new file mode 100644 index 0000000000..ceed47c3a0 --- /dev/null +++ b/server_addon/celaction/client/ayon_celaction/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring AYON addon 'celaction' version.""" +__version__ = "0.2.0" From ddb4da0e869ed00ed8ac09cf382431cc58e328cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:36:59 +0200 Subject: [PATCH 61/65] added version to cealaction addon class --- server_addon/celaction/client/ayon_celaction/__init__.py | 3 +++ server_addon/celaction/client/ayon_celaction/addon.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server_addon/celaction/client/ayon_celaction/__init__.py b/server_addon/celaction/client/ayon_celaction/__init__.py index 8983d48d7d..0df0224125 100644 --- a/server_addon/celaction/client/ayon_celaction/__init__.py +++ b/server_addon/celaction/client/ayon_celaction/__init__.py @@ -1,3 +1,4 @@ +from .version import __version__ from .addon import ( CELACTION_ROOT_DIR, CelactionAddon, @@ -5,6 +6,8 @@ from .addon import ( __all__ = ( + "__version__", + "CELACTION_ROOT_DIR", "CelactionAddon", ) diff --git a/server_addon/celaction/client/ayon_celaction/addon.py b/server_addon/celaction/client/ayon_celaction/addon.py index d00401a2e0..ad04a54088 100644 --- a/server_addon/celaction/client/ayon_celaction/addon.py +++ b/server_addon/celaction/client/ayon_celaction/addon.py @@ -1,11 +1,14 @@ import os from ayon_core.addon import AYONAddon, IHostAddon +from .version import __version__ + CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) class CelactionAddon(AYONAddon, IHostAddon): name = "celaction" + version = __version__ host_name = "celaction" def get_launch_hook_paths(self, app): From 230a7c1ac34a64268c79a0d557402c69f236a3ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:37:46 +0200 Subject: [PATCH 62/65] added 'version' to addon class --- server_addon/flame/client/ayon_flame/__init__.py | 3 +++ server_addon/flame/client/ayon_flame/addon.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server_addon/flame/client/ayon_flame/__init__.py b/server_addon/flame/client/ayon_flame/__init__.py index c7b4516c95..d2d89bdb01 100644 --- a/server_addon/flame/client/ayon_flame/__init__.py +++ b/server_addon/flame/client/ayon_flame/__init__.py @@ -1,3 +1,4 @@ +from .version import __version__ from .addon import ( FLAME_ADDON_ROOT, FlameAddon, @@ -5,6 +6,8 @@ from .addon import ( __all__ = ( + "__version__", + "FLAME_ADDON_ROOT", "FlameAddon", ) diff --git a/server_addon/flame/client/ayon_flame/addon.py b/server_addon/flame/client/ayon_flame/addon.py index 2a9c57b24c..5a96a9332e 100644 --- a/server_addon/flame/client/ayon_flame/addon.py +++ b/server_addon/flame/client/ayon_flame/addon.py @@ -1,11 +1,14 @@ import os from ayon_core.addon import AYONAddon, IHostAddon +from .version import __version__ + FLAME_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class FlameAddon(AYONAddon, IHostAddon): name = "flame" + version = __version__ host_name = "flame" def add_implementation_envs(self, env, _app): From 80966a5532eacbbf9b7d5aee6e4613180b072286 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 May 2024 11:38:51 +0200 Subject: [PATCH 63/65] fix typo in product types label --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ef531c8345..2640a3db37 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -558,7 +558,7 @@ class ExtractBurninProfile(BaseSettingsModel): _layout = "expanded" product_types: list[str] = SettingsField( default_factory=list, - title="Produt types" + title="Product types" ) hosts: list[str] = SettingsField( default_factory=list, From 4b593e9273176255395b68d71cfd6373b9410d30 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 30 May 2024 19:54:22 +0800 Subject: [PATCH 64/65] Update server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py --- .../ayon_substancepainter/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py index 0bf451f0e2..b100e4189d 100644 --- a/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py +++ b/server_addon/substancepainter/client/ayon_substancepainter/plugins/create/create_workfile.py @@ -16,7 +16,7 @@ import substance_painter.project class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" - identifier = "io.ayon.creators.substancepainter.workfile" + identifier = "io.openpype.creators.substancepainter.workfile" label = "Workfile" product_type = "workfile" icon = "document" From c5f385092f218c3847b5763a700601a41da1279d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 May 2024 20:54:32 +0800 Subject: [PATCH 65/65] add the missing addon milestone versions --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 01f80267d8..5a2c1d353e 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -54,6 +54,7 @@ MOVED_ADDON_MILESTONE_VERSIONS = { "celaction": VersionInfo(0, 2, 0), "clockify": VersionInfo(0, 2, 0), "flame": VersionInfo(0, 2, 0), + "max": VersionInfo(0, 2, 0), "traypublisher": VersionInfo(0, 2, 0), "tvpaint": VersionInfo(0, 2, 0), "nuke": VersionInfo(0, 2, 0),