From 765b8a5213d54def16266a1744d31ecdaf494987 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:57:44 +0100 Subject: [PATCH 001/100] move maya capture preset to plugin --- .../maya/plugins/publish/extract_playblast.py | 6 +- .../maya/plugins/publish/extract_thumbnail.py | 2 +- .../defaults/project_settings/maya.json | 230 +++---- .../schemas/schema_maya_capture.json | 581 ----------------- .../schemas/template_maya_capture.json | 588 ++++++++++++++++++ 5 files changed, 713 insertions(+), 694 deletions(-) delete mode 100644 pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json create mode 100644 pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json diff --git a/pype/hosts/maya/plugins/publish/extract_playblast.py b/pype/hosts/maya/plugins/publish/extract_playblast.py index 770b077e41..da1428400e 100644 --- a/pype/hosts/maya/plugins/publish/extract_playblast.py +++ b/pype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,6 +23,7 @@ class ExtractPlayblast(pype.api.Extractor): hosts = ["maya"] families = ["review"] optional = True + capture_preset = {} def process(self, instance): self.log.info("Extracting capture..") @@ -43,12 +44,9 @@ class ExtractPlayblast(pype.api.Extractor): # get cameras camera = instance.data['review_camera'] - capture_preset = ( - instance.context.data['project_settings']['maya']['capture'] - ) try: - preset = lib.load_capture_preset(data=capture_preset) + preset = lib.load_capture_preset(data=self.capture_preset) except Exception: preset = {} self.log.info('using viewport preset: {}'.format(preset)) diff --git a/pype/hosts/maya/plugins/publish/extract_thumbnail.py b/pype/hosts/maya/plugins/publish/extract_thumbnail.py index 49511f6af6..9f6d16275f 100644 --- a/pype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/pype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -34,7 +34,7 @@ class ExtractThumbnail(pype.api.Extractor): capture_preset = "" capture_preset = ( - instance.context.data["project_settings"]['maya']['capture'] + instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast'] ) try: diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index 03955732d2..c1f43ff81e 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -1,111 +1,11 @@ { - "capture": { - "Codec": { - "compression": "jpg", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 0.7, - 0.7, - 0.7 - ], - "backgroundBottom": [ - 0.7, - 0.7, - 0.7 - ], - "backgroundTop": [ - 0.7, - 0.7, - 0.7 - ], - "override_display": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true - }, - "IO": { - "name": "", - "open_finished": true, - "raw_frame_numbers": true, - "recent_playblasts": [], - "save_file": true - }, - "PanZoom": { - "pan_zoom": true - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 1080, - "height": 1920, - "percent": 1.0, - "mode": "Custom" - }, - "Time Range": { - "start_frame": 0, - "end_frame": 0, - "frame": "", - "time": "Time Slider" - }, - "Viewport Options": { - "cameras": false, - "clipGhosts": false, - "controlVertices": false, - "deformers": false, - "dimensions": false, - "displayLights": 0, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "gpuCacheDisplayFilter": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "high_quality": true, - "hud": false, - "hulls": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "nurbsCurves": false, - "nurbsSurfaces": false, - "override_viewport_options": true, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "shadows": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false, - "twoSidedLighting": true - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 - } + "ext_mapping": { + "model": "ma", + "mayaAscii": "ma", + "camera": "ma", + "rig": "ma", + "workfile": "ma", + "yetiRig": "ma" }, "create": { "CreateAnimation": { @@ -299,6 +199,10 @@ "enabled": false, "optional": true }, + "ValidateMeshNormalsUnlocked": { + "enabled": false, + "optional": true + }, "ValidateMeshUVSetMap1": { "enabled": false, "optional": true @@ -336,7 +240,7 @@ "optional": true }, "ValidateTransformZero": { - "enabled": true, + "enabled": false, "optional": true }, "ValidateCameraAttributes": { @@ -351,6 +255,116 @@ "enabled": true, "optional": true }, + "ExtractPlayblast": { + "capture_preset": { + "Codec": { + "compression": "jpg", + "format": "image", + "quality": 95 + }, + "Display Options": { + "background": [ + 0.7, + 0.7, + 0.7 + ], + "backgroundBottom": [ + 0.7, + 0.7, + 0.7 + ], + "backgroundTop": [ + 0.7, + 0.7, + 0.7 + ], + "override_display": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true + }, + "IO": { + "name": "", + "open_finished": true, + "raw_frame_numbers": true, + "recent_playblasts": [], + "save_file": true + }, + "PanZoom": { + "pan_zoom": true + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1080, + "height": 1920, + "percent": 1.0, + "mode": "Custom" + }, + "Time Range": { + "start_frame": 0, + "end_frame": 0, + "frame": "", + "time": "Time Slider" + }, + "Viewport Options": { + "cameras": false, + "clipGhosts": false, + "controlVertices": false, + "deformers": false, + "dimensions": false, + "displayLights": 0, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "gpuCacheDisplayFilter": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "high_quality": true, + "hud": false, + "hulls": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "nurbsCurves": false, + "nurbsSurfaces": false, + "override_viewport_options": true, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": true, + "shadows": true, + "strokes": false, + "subdivSurfaces": false, + "textures": false, + "twoSidedLighting": true + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + } + }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json deleted file mode 100644 index 4745a19075..0000000000 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ /dev/null @@ -1,581 +0,0 @@ -{ - "type": "dict", - "collapsible": true, - "key": "capture", - "label": "Maya Playblast settings", - "is_file": true, - "children": [ - { - "type": "dict", - "key": "Codec", - "children": [ - { - "type": "label", - "label": "Codec" - }, - { - "type": "text", - "key": "compression", - "label": "Compression type" - }, - { - "type": "text", - "key": "format", - "label": "Data format" - }, - { - "type": "number", - "key": "quality", - "label": "Quality", - "decimal": 0, - "minimum": 0, - "maximum": 100 - }, - - { - "type": "splitter" - } - ] - }, - { - "type": "dict", - "key": "Display Options", - "children": [ - { - "type": "label", - "label": "Display Options" - }, - { - "type": "list-strict", - "key": "background", - "label": "Background Color: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundBottom", - "label": "Background Bottom: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundTop", - "label": "Background Top: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Generic", - "children": [ - { - "type": "label", - "label": "Generic" - }, - { - "type": "boolean", - "key": "isolate_view", - "label": " Isolate view" - }, - { - "type": "boolean", - "key": "off_screen", - "label": " Off Screen" - } - ] - }, - { - "type": "dict", - "key": "IO", - "children": [ - { - "type": "label", - "label": "IO" - }, - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "open_finished", - "label": "Open finished" - }, - { - "type": "boolean", - "key": "raw_frame_numbers", - "label": "Raw frame numbers" - }, - { - "type": "list", - "key": "recent_playblasts", - "label": "Recent Playblasts", - "object_type": "text" - }, - { - "type": "boolean", - "key": "save_file", - "label": "Save file" - } - ] - }, - { - "type": "dict", - "key": "PanZoom", - "children": [ - { - "type": "boolean", - "key": "pan_zoom", - "label": " Pan Zoom" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Renderer", - "children": [ - { - "type": "label", - "label": "Renderer" - }, - { - "type": "text", - "key": "rendererName", - "label": " Renderer name" - } - ] - }, - { - "type": "dict", - "key": "Resolution", - "children": [ - { - "type": "splitter" - }, - { - "type": "label", - "label": "Resolution" - }, - { - "type": "number", - "key": "width", - "label": " Width", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "height", - "label": "Height", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "percent", - "label": "percent", - "decimal": 1, - "minimum": 0, - "maximum": 200 - }, - { - "type": "text", - "key": "mode", - "label": "Mode" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Time Range", - "children": [ - { - "type": "label", - "label": "Time Range" - }, - { - "type": "number", - "key": "start_frame", - "label": " Start frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "number", - "key": "end_frame", - "label": "End frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "text", - "key": "frame", - "label": "Frame" - }, - { - "type": "text", - "key": "time", - "label": "Time" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Viewport Options", - "label": "Viewport Options", - "children": [ - { - "type": "boolean", - "key": "cameras", - "label": "cameras" - }, - { - "type": "boolean", - "key": "clipGhosts", - "label": "clipGhosts" - }, - { - "type": "boolean", - "key": "controlVertices", - "label": "controlVertices" - }, - { - "type": "boolean", - "key": "deformers", - "label": "deformers" - }, - { - "type": "boolean", - "key": "dimensions", - "label": "dimensions" - }, - { - "type": "number", - "key": "displayLights", - "label": "displayLights", - "decimal": 0, - "minimum": 0, - "maximum": 10 - }, - { - "type": "boolean", - "key": "dynamicConstraints", - "label": "dynamicConstraints" - }, - { - "type": "boolean", - "key": "dynamics", - "label": "dynamics" - }, - { - "type": "boolean", - "key": "fluids", - "label": "fluids" - }, - { - "type": "boolean", - "key": "follicles", - "label": "follicles" - }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "gpuCacheDisplayFilter" - }, - { - "type": "boolean", - "key": "greasePencils", - "label": "greasePencils" - }, - { - "type": "boolean", - "key": "grid", - "label": "grid" - }, - { - "type": "boolean", - "key": "hairSystems", - "label": "hairSystems" - }, - { - "type": "boolean", - "key": "handles", - "label": "handles" - }, - { - "type": "boolean", - "key": "high_quality", - "label": "high_quality" - }, - { - "type": "boolean", - "key": "hud", - "label": "hud" - }, - { - "type": "boolean", - "key": "hulls", - "label": "hulls" - }, - { - "type": "boolean", - "key": "ikHandles", - "label": "ikHandles" - }, - { - "type": "boolean", - "key": "imagePlane", - "label": "imagePlane" - }, - { - "type": "boolean", - "key": "joints", - "label": "joints" - }, - { - "type": "boolean", - "key": "lights", - "label": "lights" - }, - { - "type": "boolean", - "key": "locators", - "label": "locators" - }, - { - "type": "boolean", - "key": "manipulators", - "label": "manipulators" - }, - { - "type": "boolean", - "key": "motionTrails", - "label": "motionTrails" - }, - { - "type": "boolean", - "key": "nCloths", - "label": "nCloths" - }, - { - "type": "boolean", - "key": "nParticles", - "label": "nParticles" - }, - { - "type": "boolean", - "key": "nRigids", - "label": "nRigids" - }, - { - "type": "boolean", - "key": "nurbsCurves", - "label": "nurbsCurves" - }, - { - "type": "boolean", - "key": "nurbsSurfaces", - "label": "nurbsSurfaces" - }, - { - "type": "boolean", - "key": "override_viewport_options", - "label": "override_viewport_options" - }, - { - "type": "boolean", - "key": "particleInstancers", - "label": "particleInstancers" - }, - { - "type": "boolean", - "key": "pivots", - "label": "pivots" - }, - { - "type": "boolean", - "key": "planes", - "label": "planes" - }, - { - "type": "boolean", - "key": "pluginShapes", - "label": "pluginShapes" - }, - { - "type": "boolean", - "key": "polymeshes", - "label": "polymeshes" - }, - { - "type": "boolean", - "key": "shadows", - "label": "shadows" - }, - { - "type": "boolean", - "key": "strokes", - "label": "strokes" - }, - { - "type": "boolean", - "key": "subdivSurfaces", - "label": "subdivSurfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "textures" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "twoSidedLighting" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Camera Options", - "label": "Camera Options", - "children": [ - { - "type": "boolean", - "key": "displayGateMask", - "label": "displayGateMask" - }, - { - "type": "boolean", - "key": "displayResolution", - "label": "displayResolution" - }, - { - "type": "boolean", - "key": "displayFilmGate", - "label": "displayFilmGate" - }, - { - "type": "boolean", - "key": "displayFieldChart", - "label": "displayFieldChart" - }, - { - "type": "boolean", - "key": "displaySafeAction", - "label": "displaySafeAction" - }, - { - "type": "boolean", - "key": "displaySafeTitle", - "label": "displaySafeTitle" - }, - { - "type": "boolean", - "key": "displayFilmPivot", - "label": "displayFilmPivot" - }, - { - "type": "boolean", - "key": "displayFilmOrigin", - "label": "displayFilmOrigin" - }, - { - "type": "number", - "key": "overscan", - "label": "overscan", - "decimal": 1, - "minimum": 0, - "maximum": 10 - } - ] - } - ] -} diff --git a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json new file mode 100644 index 0000000000..f6fcb3b998 --- /dev/null +++ b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json @@ -0,0 +1,588 @@ +[ + { + "type": "dict", + "collapsible": true, + "key": "ExtractPlayblast", + "label": "Extract Playblast settings", + "children": [ + { + "type": "dict", + "key": "capture_preset", + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Compression type" + }, + { + "type": "text", + "key": "format", + "label": "Data format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, + + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, + { + "type": "list-strict", + "key": "background", + "label": "Background Color: ", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + }, + { + "type": "list-strict", + "key": "backgroundBottom", + "label": "Background Bottom: ", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + }, + { + "type": "list-strict", + "key": "backgroundTop", + "label": "Background Top: ", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + } + ] + }, + { + "type": "dict", + "key": "IO", + "children": [ + { + "type": "label", + "label": "IO" + }, + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "boolean", + "key": "open_finished", + "label": "Open finished" + }, + { + "type": "boolean", + "key": "raw_frame_numbers", + "label": "Raw frame numbers" + }, + { + "type": "list", + "key": "recent_playblasts", + "label": "Recent Playblasts", + "object_type": "text" + }, + { + "type": "boolean", + "key": "save_file", + "label": "Save file" + } + ] + }, + { + "type": "dict", + "key": "PanZoom", + "children": [ + { + "type": "boolean", + "key": "pan_zoom", + "label": " Pan Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "text", + "key": "rendererName", + "label": " Renderer name" + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "percent", + "label": "percent", + "decimal": 1, + "minimum": 0, + "maximum": 200 + }, + { + "type": "text", + "key": "mode", + "label": "Mode" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Time Range", + "children": [ + { + "type": "label", + "label": "Time Range" + }, + { + "type": "number", + "key": "start_frame", + "label": " Start frame", + "decimal": 0, + "minimum": 0, + "maximum": 999999 + }, + { + "type": "number", + "key": "end_frame", + "label": "End frame", + "decimal": 0, + "minimum": 0, + "maximum": 999999 + }, + { + "type": "text", + "key": "frame", + "label": "Frame" + }, + { + "type": "text", + "key": "time", + "label": "Time" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "cameras", + "label": "cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "clipGhosts" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "controlVertices" + }, + { + "type": "boolean", + "key": "deformers", + "label": "deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "dimensions" + }, + { + "type": "number", + "key": "displayLights", + "label": "displayLights", + "decimal": 0, + "minimum": 0, + "maximum": 10 + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "dynamicConstraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "follicles" + }, + { + "type": "boolean", + "key": "gpuCacheDisplayFilter", + "label": "gpuCacheDisplayFilter" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "greasePencils" + }, + { + "type": "boolean", + "key": "grid", + "label": "grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "hairSystems" + }, + { + "type": "boolean", + "key": "handles", + "label": "handles" + }, + { + "type": "boolean", + "key": "high_quality", + "label": "high_quality" + }, + { + "type": "boolean", + "key": "hud", + "label": "hud" + }, + { + "type": "boolean", + "key": "hulls", + "label": "hulls" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "ikHandles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "imagePlane" + }, + { + "type": "boolean", + "key": "joints", + "label": "joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "motionTrails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "nurbsCurves" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "nurbsSurfaces" + }, + { + "type": "boolean", + "key": "override_viewport_options", + "label": "override_viewport_options" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "particleInstancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "pluginShapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "polymeshes" + }, + { + "type": "boolean", + "key": "shadows", + "label": "shadows" + }, + { + "type": "boolean", + "key": "strokes", + "label": "strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "subdivSurfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "textures" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "twoSidedLighting" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "displayGateMask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "displayResolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "displayFilmGate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "displayFieldChart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "displaySafeAction" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "displaySafeTitle" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "displayFilmPivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "displayFilmOrigin" + }, + { + "type": "number", + "key": "overscan", + "label": "overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + } + ] + } +] From d1aebe70bc611def48a606f785d62348a0390900 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:57:58 +0100 Subject: [PATCH 002/100] return extension mapping to maya settings --- .../schemas/projects_schema/schema_project_maya.json | 9 +++++++-- .../projects_schema/schemas/schema_maya_publish.json | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_maya.json b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json index 7a270b0046..0a59cab510 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -6,8 +6,13 @@ "is_file": true, "children": [ { - "type": "schema", - "name": "schema_maya_capture" + "type": "dict-modifiable", + "key": "ext_mapping", + "label": "Extension Mapping", + "use_label_wrap": true, + "object_type": { + "type": "text" + } }, { "type": "schema", diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 58a21c185a..6ecda224ea 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -170,6 +170,10 @@ "key": "ValidateMeshNonManifold", "label": "ValidateMeshNonManifold" }, + { + "key": "ValidateMeshNormalsUnlocked", + "label": "ValidateMeshNormalsUnlocked" + }, { "key": "ValidateMeshUVSetMap1", "label": "ValidateMeshUVSetMap1", @@ -242,6 +246,10 @@ "type": "label", "label": "Extractors" }, + { + "type": "schema_template", + "name": "template_maya_capture" + }, { "type": "dict", "collapsible": true, From a378bb4bd02c8e9099213cdc9dd543b300b1f3da Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:09 +0100 Subject: [PATCH 003/100] maya wrong api import --- pype/hosts/maya/api/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/api/action.py b/pype/hosts/maya/api/action.py index 35a57a4445..f623298451 100644 --- a/pype/hosts/maya/api/action.py +++ b/pype/hosts/maya/api/action.py @@ -72,7 +72,7 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): nodes (list): all nodes to regenerate ids on """ - from pype.hosts.maya import lib + from pype.hosts.maya.api import lib import avalon.io as io asset = instance.data['asset'] From 97324bc5e306559e6d4f737ac81edee67f6c1da8 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:23 +0100 Subject: [PATCH 004/100] add nuke validator defaults --- .../defaults/project_settings/nuke.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/pype/settings/defaults/project_settings/nuke.json b/pype/settings/defaults/project_settings/nuke.json index 517065f79a..392f189bd7 100644 --- a/pype/settings/defaults/project_settings/nuke.json +++ b/pype/settings/defaults/project_settings/nuke.json @@ -11,6 +11,30 @@ "PreCollectNukeInstances": { "sync_workfile_version": true }, + "ValidateKnobs": { + "enabled": false, + "knobs": { + "render": { + "review": true + } + } + }, + "ValidateOutputResolution": { + "enabled": true, + "optional": true + }, + "ValidateGizmo": { + "enabled": true, + "optional": true + }, + "ValidateScript": { + "enabled": true, + "optional": true + }, + "ValidateNukeWriteBoundingBox": { + "enabled": true, + "optional": true + }, "ExtractThumbnail": { "enabled": true, "nodes": { @@ -38,14 +62,6 @@ ] } }, - "ValidateKnobs": { - "enabled": false, - "knobs": { - "render": { - "review": true - } - } - }, "ExtractReviewDataLut": { "enabled": false }, @@ -61,22 +77,6 @@ "deadline_pool": "", "deadline_pool_secondary": "", "deadline_chunk_size": 1 - }, - "ValidateOutputResolution": { - "enabled": true, - "optional": true - }, - "ValidateGizmo": { - "enabled": true, - "optional": true - }, - "ValidateScript": { - "enabled": true, - "optional": true - }, - "ValidateNukeWriteBoundingBox": { - "enabled": true, - "optional": true } }, "workfile_build": { From 4ca92280a78bc5bc98d79d0affbf6d20988566e6 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:38 +0100 Subject: [PATCH 005/100] remove standalone publisher thumbnail default --- .../project_settings/standalonepublisher.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/settings/defaults/project_settings/standalonepublisher.json b/pype/settings/defaults/project_settings/standalonepublisher.json index ad1b5e82b2..08895bcba9 100644 --- a/pype/settings/defaults/project_settings/standalonepublisher.json +++ b/pype/settings/defaults/project_settings/standalonepublisher.json @@ -1,14 +1,4 @@ { - "publish": { - "ExtractThumbnailSP": { - "ffmpeg_args": { - "input": [ - "gamma 2.2" - ], - "output": [] - } - } - }, "create": { "create_workfile": { "name": "workfile", @@ -121,5 +111,15 @@ "create_image": "Image", "create_matchmove": "Matchmove" } + }, + "publish": { + "ExtractThumbnailSP": { + "ffmpeg_args": { + "input": [ + "gamma 2.2" + ], + "output": [] + } + } } } \ No newline at end of file From d4c1f2e4b6a00bf8c2f7169bca1d37f7cdb0158f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Mar 2021 11:17:11 +0100 Subject: [PATCH 006/100] PS - added .psb support --- pype/hosts/photoshop/plugins/publish/collect_workfile.py | 5 +++-- pype/hosts/photoshop/plugins/publish/increment_workfile.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/hosts/photoshop/plugins/publish/collect_workfile.py b/pype/hosts/photoshop/plugins/publish/collect_workfile.py index 766be02354..88817c3969 100644 --- a/pype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/pype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -31,9 +31,10 @@ class CollectWorkfile(pyblish.api.ContextPlugin): }) # creating representation + _, ext = os.path.splitext(file_path) instance.data["representations"].append({ - "name": "psd", - "ext": "psd", + "name": ext[1:], + "ext": ext[1:], "files": base_name, "stagingDir": staging_dir, }) diff --git a/pype/hosts/photoshop/plugins/publish/increment_workfile.py b/pype/hosts/photoshop/plugins/publish/increment_workfile.py index eca2583595..2005973ea0 100644 --- a/pype/hosts/photoshop/plugins/publish/increment_workfile.py +++ b/pype/hosts/photoshop/plugins/publish/increment_workfile.py @@ -1,3 +1,4 @@ +import os import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up @@ -25,6 +26,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): ) scene_path = version_up(instance.context.data["currentFile"]) - photoshop.stub().saveAs(scene_path, 'psd', True) + _, ext = os.path.splitext(scene_path) + photoshop.stub().saveAs(scene_path, ext[1:], True) self.log.info("Incremented workfile to: {}".format(scene_path)) From 1e5d2fdd4307d52285eed9f5a444b56ee49707ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:56:54 +0100 Subject: [PATCH 007/100] removed modifiable save modes from extractor --- .../plugins/publish/extract_sequence.py | 53 ++----------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index aa625a497a..8fbf195fde 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -45,13 +45,6 @@ class ExtractSequence(pyblish.api.Extractor): "tga" } - default_save_mode = "\"PNG\"" - save_mode_for_family = { - "review": "\"PNG\"", - "renderPass": "\"PNG\"", - "renderLayer": "\"PNG\"", - } - def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) @@ -80,34 +73,15 @@ class ExtractSequence(pyblish.api.Extractor): len(layer_names), joined_layer_names ) ) - # This is plugin attribe cleanup method - self._prepare_save_modes() family_lowered = instance.data["family"].lower() - save_mode = self.save_mode_for_family.get( - family_lowered, self.default_save_mode - ) - save_mode_type = self._get_save_mode_type(save_mode) - - if not bool(save_mode_type in self.sequential_save_mode): - raise AssertionError(( - "Plugin can export only sequential frame output" - " but save mode for family \"{}\" is not for sequence > {} <" - ).format(instance.data["family"], save_mode)) - frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - filename_template = self._get_filename_template( - save_mode_type, save_mode, frame_end - ) + filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") - self.log.debug( - "Using save mode > {} < and file template \"{}\"".format( - save_mode, filename_template - ) - ) + self.log.debug("Using file template \"{}\"".format(filename_template)) # Save to staging dir output_dir = instance.data.get("stagingDir") @@ -186,19 +160,6 @@ class ExtractSequence(pyblish.api.Extractor): } instance.data["representations"].append(thumbnail_repre) - def _prepare_save_modes(self): - """Lower family names in keys and skip empty values.""" - new_specifications = {} - for key, value in self.save_mode_for_family.items(): - if value: - new_specifications[key.lower()] = value - else: - self.log.warning(( - "Save mode for family \"{}\" has empty value." - " The family will use default save mode: > {} <." - ).format(key, self.default_save_mode)) - self.save_mode_for_family = new_specifications - def _get_save_mode_type(self, save_mode): """Extract type of save mode. @@ -212,7 +173,7 @@ class ExtractSequence(pyblish.api.Extractor): self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) return save_mode_type - def _get_filename_template(self, save_mode_type, save_mode, frame_end): + def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. This is simple template contains `{frame}{ext}` for sequential outputs @@ -220,18 +181,12 @@ class ExtractSequence(pyblish.api.Extractor): temporary folder so filename should not matter as integrator change them. """ - ext = self.save_mode_to_ext.get(save_mode_type) - if ext is None: - raise AssertionError(( - "Couldn't find file extension for TVPaint's save mode: > {} <" - ).format(save_mode)) - frame_padding = 4 frame_end_str_len = len(str(frame_end)) if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{frame:0>{}}}".format(frame_padding) + ext + return "{{:0>{}}}".format(frame_padding) + ".png" def render( self, save_mode, filename_template, output_dir, layers, From 0ab660929661f9b10f68f3c24508f046cc5c3f33 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:57:50 +0100 Subject: [PATCH 008/100] imlpemented method for copying same temp image files --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8fbf195fde..9755bb2850 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -317,3 +317,10 @@ class ExtractSequence(pyblish.api.Extractor): ) filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From 8654bcac7cd70614dd6490119f97892dc345cafa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:59:35 +0100 Subject: [PATCH 009/100] implemented method that will fill frames by pre behavior of a layer --- .../plugins/publish/extract_sequence.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 9755bb2850..87d5dfc9cc 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -318,6 +318,64 @@ class ExtractSequence(pyblish.api.Extractor): filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 8a53aa4827cdd21e9cca8ec239513e049f28e92c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:00:17 +0100 Subject: [PATCH 010/100] implemented method that will fill frames by post behavior of a layer --- .../plugins/publish/extract_sequence.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 87d5dfc9cc..0f1ce8691a 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -376,6 +376,61 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 0ecdfb618b8dbe194882d5347ef2d18dc90a1aac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:01:26 +0100 Subject: [PATCH 011/100] implemented logic of layers compositing using Pillow --- .../plugins/publish/extract_sequence.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 0f1ce8691a..449da3c1e0 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -4,6 +4,7 @@ import tempfile import pyblish.api from avalon.tvpaint import lib +from PIL import Image class ExtractSequence(pyblish.api.Extractor): @@ -431,6 +432,51 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + img_obj = None + for image_filepath in image_filepaths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + continue + + img_obj.alpha_composite(_img_obj) + img_obj.save(output_filepath) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 9d624641d4f1b7650a66fe6fc629b7f051c90445 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:02:47 +0100 Subject: [PATCH 012/100] frame start/end are defined by mark in/out of published clip --- .../plugins/publish/collect_workfile_data.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index bd2e574518..f25e274581 100644 --- a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -113,7 +113,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - frame_start = int(workfile_info_parts.pop(-1)) + _frame_start = int(workfile_info_parts.pop(-1)) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -121,21 +121,14 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): width = int(workfile_info_parts.pop(-1)) workfile_path = " ".join(workfile_info_parts).replace("\"", "") - # TODO This is not porper way of getting last frame - # - but don't know better - last_frame = frame_start - for layer in layers_data: - frame_end = layer["frame_end"] - if frame_end > last_frame: - last_frame = frame_end - + frame_start, frame_end = self.collect_clip_frames() scene_data = { "currentFile": workfile_path, "sceneWidth": width, "sceneHeight": height, "pixelAspect": pixel_apsect, "frameStart": frame_start, - "frameEnd": last_frame, + "frameEnd": frame_end, "fps": frame_rate, "fieldOrder": field_order } @@ -143,3 +136,19 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def collect_clip_frames(self): + clip_info_str = lib.execute_george("tv_clipinfo") + self.log.debug("Clip info: {}".format(clip_info_str)) + clip_info_items = clip_info_str.split(" ") + # Color index + color_idx = clip_info_items.pop(-1) + clip_info_items.pop(-1) + + mark_out = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + mark_in = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + return mark_in, mark_out From e4fc43a1f6b9ba8f666b4ace7b53fa29b85177e8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:28 +0100 Subject: [PATCH 013/100] collect both layer's position and all layer ids --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 449da3c1e0..576d294c42 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -214,10 +214,13 @@ class ExtractSequence(pyblish.api.Extractor): save_mode = "tv_SaveMode {}".format(save_mode) # Map layers by position - layers_by_position = { - layer["position"]: layer - for layer in layers - } + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) # Sort layer positions in reverse order sorted_positions = list(reversed(sorted(layers_by_position.keys()))) From 1bf02c515347ec0c350618c22aa2aa6d5755f33f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:55 +0100 Subject: [PATCH 014/100] skip savemode filling --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 576d294c42..6efa22d1cd 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -190,8 +190,8 @@ class ExtractSequence(pyblish.api.Extractor): return "{{:0>{}}}".format(frame_padding) + ".png" def render( - self, save_mode, filename_template, output_dir, layers, - first_frame, last_frame, thumbnail_filename + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename ): """ Export images from TVPaint. @@ -210,9 +210,6 @@ class ExtractSequence(pyblish.api.Extractor): dict: Mapping frame to output filepath. """ - # Add save mode arguments to function - save_mode = "tv_SaveMode {}".format(save_mode) - # Map layers by position layers_by_position = {} layer_ids = [] From e1d961328f97aec0b86d25cf80396ba1dee0cd78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:00 +0100 Subject: [PATCH 015/100] removed previous logic of rendering --- .../plugins/publish/extract_sequence.py | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 6efa22d1cd..0fe4018157 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -102,17 +102,6 @@ class ExtractSequence(pyblish.api.Extractor): save_mode, filename_template, output_dir, filtered_layers, frame_start, frame_end, thumbnail_filename ) - thumbnail_fullpath = output_files_by_frame.pop( - thumbnail_filename, None - ) - - # Fill gaps in sequence - self.fill_missing_frames( - output_files_by_frame, - frame_start, - frame_end, - filename_template - ) # Fill tags and new families tags = [] @@ -224,100 +213,9 @@ class ExtractSequence(pyblish.api.Extractor): if not sorted_positions: return - # Create temporary layer - new_layer_id = lib.execute_george("tv_layercreate _tmp_layer") - # Merge layers to temp layer - george_script_lines = [] - # Set duplicated layer as current - george_script_lines.append("tv_layerset {}".format(new_layer_id)) for position in sorted_positions: layer = layers_by_position[position] - george_script_lines.append( - "tv_layermerge {}".format(layer["layer_id"]) - ) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Frames with keyframe - exposure_frames = lib.get_exposure_frames( - new_layer_id, first_frame, last_frame - ) - - # TODO what if there is not exposue frames? - # - this force to have first frame all the time - if first_frame not in exposure_frames: - exposure_frames.insert(0, first_frame) - - # Restart george script lines - george_script_lines = [] - george_script_lines.append(save_mode) - - all_output_files = {} - for frame in exposure_frames: - filename = filename_template.format(frame, frame=frame) - dst_path = "/".join([output_dir, filename]) - all_output_files[frame] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - # Export thumbnail - if thumbnail_filename: - basename, ext = os.path.splitext(thumbnail_filename) - if not ext: - ext = ".jpg" - thumbnail_fullpath = "/".join([output_dir, basename + ext]) - all_output_files[thumbnail_filename] = thumbnail_fullpath - # Force save mode to png for thumbnail - george_script_lines.append("tv_SaveMode \"JPG\"") - # Go to frame - george_script_lines.append("tv_layerImage {}".format(first_frame)) - # Store image to output - george_script_lines.append( - "tv_saveimage \"{}\"".format(thumbnail_fullpath) - ) - - # Delete temporary layer - george_script_lines.append("tv_layerkill {}".format(new_layer_id)) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - return all_output_files - - def fill_missing_frames( - self, filepaths_by_frame, first_frame, last_frame, filename_template - ): - """Fill not rendered frames with previous frame. - - Extractor is rendering only frames with keyframes (exposure frames) to - get output faster which means there may be gaps between frames. - This function fill the missing frames. - """ - output_dir = None - previous_frame_filepath = None - for frame in range(first_frame, last_frame + 1): - if frame in filepaths_by_frame: - previous_frame_filepath = filepaths_by_frame[frame] - continue - - elif previous_frame_filepath is None: - self.log.warning( - "No frames to fill. Seems like nothing was exported." - ) - break - - if output_dir is None: - output_dir = os.path.dirname(previous_frame_filepath) - - filename = filename_template.format(frame=frame) - space_filepath = os.path.normpath( - os.path.join(output_dir, filename) - ) - filepaths_by_frame[frame] = space_filepath - shutil.copy(previous_frame_filepath, space_filepath) def _fill_frame_by_pre_behavior( self, From 4ca8b47649a1a28bb4d13f01d5801da9c408a68a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:38 +0100 Subject: [PATCH 016/100] collect behavior of layer ids to process --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 0fe4018157..64bd023f5f 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -213,6 +213,7 @@ class ExtractSequence(pyblish.api.Extractor): if not sorted_positions: return + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) for position in sorted_positions: layer = layers_by_position[position] From 7b1e4fe0fd6a1e371cb7ffe539538dc6ee6a1e82 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:09:32 +0100 Subject: [PATCH 017/100] implemented method that will render and fill all frames of given layer --- .../plugins/publish/extract_sequence.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 64bd023f5f..852ec01183 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -218,6 +218,80 @@ class ExtractSequence(pyblish.api.Extractor): for position in sorted_positions: layer = layers_by_position[position] + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + def _fill_frame_by_pre_behavior( self, layer, From 6eeeb721a682a73fc7bef1e1433cc7ba2f6ad81d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:13 +0100 Subject: [PATCH 018/100] layers are rendered one by one and stored by their position (order) --- .../tvpaint/plugins/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 852ec01183..e1667997fb 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -215,8 +215,25 @@ class ExtractSequence(pyblish.api.Extractor): behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} for position in sorted_positions: layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + def render_layer( self, From a2e2d1fb3f112f7528b824a4ed89bf72e8bd70f2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:49 +0100 Subject: [PATCH 019/100] rendered frames are composite to final output --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index e1667997fb..919dd02f7c 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -234,6 +234,15 @@ class ExtractSequence(pyblish.api.Extractor): ) files_by_position[position] = files_by_frames + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename + ) + return output def render_layer( self, From 5779158d34984d984e5302b2ec89e5b51edfcf5a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:11:14 +0100 Subject: [PATCH 020/100] added cleanup method that will remove temp image files of individial layers --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 919dd02f7c..f1929707b4 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -242,6 +242,7 @@ class ExtractSequence(pyblish.api.Extractor): filename_template, thumbnail_filename ) + self._cleanup_tmp_files(files_by_position) return output def render_layer( @@ -476,6 +477,11 @@ class ExtractSequence(pyblish.api.Extractor): return output_filepaths, thumbnail_filepath + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 7973229bff1654afea8b75c38ae53a00e62bf0b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:03 +0100 Subject: [PATCH 021/100] pass different arguments and expect different output of render method --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index f1929707b4..f8aeace617 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -95,12 +95,12 @@ class ExtractSequence(pyblish.api.Extractor): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail" + thumbnail_filename = "thumbnail.jpg" # Render output - output_files_by_frame = self.render( - save_mode, filename_template, output_dir, - filtered_layers, frame_start, frame_end, thumbnail_filename + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename ) # Fill tags and new families @@ -110,7 +110,7 @@ class ExtractSequence(pyblish.api.Extractor): repre_files = [ os.path.basename(filepath) - for filepath in output_files_by_frame.values() + for filepath in output_filepaths ] # Sequence of one frame if len(repre_files) == 1: From 260088306d37f2d60241ca108b60e08825a4e97a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:17 +0100 Subject: [PATCH 022/100] keep frame start/end as they are --- pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index f8aeace617..821b212f83 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -121,8 +121,8 @@ class ExtractSequence(pyblish.api.Extractor): "ext": ext, "files": repre_files, "stagingDir": output_dir, - "frameStart": frame_start + 1, - "frameEnd": frame_end + 1, + "frameStart": frame_start, + "frameEnd": frame_end, "tags": tags } self.log.debug("Creating new representation: {}".format(new_repre)) From 018512b280ae59b069345f32337514e9d5769595 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:26:43 +0100 Subject: [PATCH 023/100] added some extra logs --- .../tvpaint/plugins/publish/extract_sequence.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 821b212f83..729eb5a948 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -61,7 +61,7 @@ class ExtractSequence(pyblish.api.Extractor): layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( - f"None of the layers from the instance" + "None of the layers from the instance" " are visible. Extraction skipped." ) return @@ -198,6 +198,7 @@ class ExtractSequence(pyblish.api.Extractor): Retruns: dict: Mapping frame to output filepath. """ + self.log.debug("Preparing data for rendering.") # Map layers by position layers_by_position = {} @@ -213,6 +214,7 @@ class ExtractSequence(pyblish.api.Extractor): if not sorted_positions: return + self.log.debug("Collecting pre/post behavior of individual layers.") behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) mark_in_index = frame_start - 1 @@ -279,10 +281,17 @@ class ExtractSequence(pyblish.api.Extractor): # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -300,6 +309,11 @@ class ExtractSequence(pyblish.api.Extractor): # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + # Pre behavior self._fill_frame_by_pre_behavior( layer, From 9baf9aaf2b4421e27d17354727efd2476b16c613 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 11:54:26 +0100 Subject: [PATCH 024/100] fixed case when all layers miss image for frame --- .../plugins/publish/extract_sequence.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 729eb5a948..847292814d 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -7,6 +7,22 @@ from avalon.tvpaint import lib from PIL import Image +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) + + class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] @@ -78,6 +94,8 @@ class ExtractSequence(pyblish.api.Extractor): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,7 +118,8 @@ class ExtractSequence(pyblish.api.Extractor): # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ) # Fill tags and new families @@ -180,7 +199,8 @@ class ExtractSequence(pyblish.api.Extractor): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ): """ Export images from TVPaint. @@ -242,7 +262,9 @@ class ExtractSequence(pyblish.api.Extractor): mark_in_index, mark_out_index, filename_template, - thumbnail_filename + thumbnail_filename, + scene_width, + scene_height ) self._cleanup_tmp_files(files_by_position) return output @@ -448,7 +470,7 @@ class ExtractSequence(pyblish.api.Extractor): def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename + filename_template, thumbnail_filename, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -465,22 +487,18 @@ class ExtractSequence(pyblish.api.Extractor): for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 + output_filename = filename_template.format(frame) output_filepath = os.path.join(output_dir, output_filename) - img_obj = None - for image_filepath in image_filepaths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - continue - - img_obj.alpha_composite(_img_obj) - img_obj.save(output_filepath) output_filepaths.append(output_filepath) if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath + composite_images( + image_filepaths, output_filepath, scene_width, scene_height + ) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From a710d12c5fc45b0614e508f3cd97c4638365573d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:05:26 +0100 Subject: [PATCH 025/100] moved composite_images to pype's tvpaint.lib --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ .../tvpaint/plugins/publish/extract_sequence.py | 17 +---------------- 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 0000000000..8172392c7f --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 847292814d..d33ec3c68c 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -4,25 +4,10 @@ import tempfile import pyblish.api from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) - - class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] From 01458f6b0bcbaa801623aa7900e9e05d4095f455 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:10:38 +0100 Subject: [PATCH 026/100] using multiprocessing to speed up compositing part --- .../plugins/publish/extract_sequence.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index d33ec3c68c..e43fb06f7a 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -1,6 +1,8 @@ import os import shutil +import time import tempfile +import multiprocessing import pyblish.api from avalon.tvpaint import lib @@ -467,6 +469,11 @@ class ExtractSequence(pyblish.api.Extractor): if frame_idx in position_data: images_by_frame[frame_idx].append(position_data[frame_idx]) + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} output_filepaths = [] thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): @@ -480,10 +487,35 @@ class ExtractSequence(pyblish.api.Extractor): if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath - composite_images( - image_filepaths, output_filepath, scene_width, scene_height + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) ) + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From b6fd172287cbfb23767cf19c37a532488ac7c689 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:35:43 +0100 Subject: [PATCH 027/100] simplified extractor with tv_savesequence command --- .../plugins/publish/extract_sequence.py | 429 +++--------------- 1 file changed, 65 insertions(+), 364 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index e43fb06f7a..17d8dc60f4 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -1,12 +1,8 @@ import os -import shutil -import time import tempfile -import multiprocessing import pyblish.api from avalon.tvpaint import lib -from pype.hosts.tvpaint.lib import composite_images from PIL import Image @@ -61,6 +57,10 @@ class ExtractSequence(pyblish.api.Extractor): for layer in layers if layer["visible"] ] + filtered_layer_ids = [ + layer["layer_id"] + for layer in filtered_layers + ] layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( @@ -81,8 +81,6 @@ class ExtractSequence(pyblish.api.Extractor): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,24 +98,53 @@ class ExtractSequence(pyblish.api.Extractor): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" + first_frame_filename = filename_template.format(frame_start) + first_frame_filepath = os.path.join(output_dir, first_frame_filename) + + # Store layers visibility + layer_visibility_by_id = {} + for layer in instance.context.data["layersData"]: + layer_id = layer["layer_id"] + layer_visibility_by_id[layer_id] = layer["visible"] + + george_script_lines = [] + for layer_id in layer_visibility_by_id.keys(): + visible = layer_id in filtered_layer_ids + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + repre_files = self.render( + filename_template, + output_dir, + frame_start, + frame_end ) + # Restore visibility + george_script_lines = [] + for layer_id, visible in layer_visibility_by_id.items(): + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if os.path.exists(first_frame_filepath): + source_img = Image.open(first_frame_filepath) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -139,36 +166,23 @@ class ExtractSequence(pyblish.api.Extractor): # Change family to render instance.data["family"] = "render" - if not thumbnail_fullpath: + if not os.path.exists(thumbnail_filepath): return thumbnail_ext = os.path.splitext( - thumbnail_fullpath + thumbnail_filepath )[1].replace(".", "") # Create thumbnail representation thumbnail_repre = { "name": "thumbnail", "ext": thumbnail_ext, "outputName": "thumb", - "files": os.path.basename(thumbnail_fullpath), + "files": os.path.basename(thumbnail_filepath), "stagingDir": output_dir, "tags": ["thumbnail"] } instance.data["representations"].append(thumbnail_repre) - def _get_save_mode_type(self, save_mode): - """Extract type of save mode. - - Helps to define output files extension. - """ - save_mode_type = ( - save_mode.lower() - .split(" ")[0] - .replace("\"", "") - ) - self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) - return save_mode_type - def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. @@ -184,356 +198,43 @@ class ExtractSequence(pyblish.api.Extractor): return "{{:0>{}}}".format(frame_padding) + ".png" - def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height - ): + def render(self, filename_template, output_dir, frame_start, frame_end): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. - first_frame (int): Starting frame from which export will begin. - last_frame (int): On which frame export will end. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. Retruns: dict: Mapping frame to output filepath. """ self.log.debug("Preparing data for rendering.") - - # Map layers by position - layers_by_position = {} - layer_ids = [] - for layer in layers: - position = layer["position"] - layers_by_position[position] = layer - - layer_ids.append(layer["layer_id"]) - - # Sort layer positions in reverse order - sorted_positions = list(reversed(sorted(layers_by_position.keys()))) - if not sorted_positions: - return - - self.log.debug("Collecting pre/post behavior of individual layers.") - behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) - - mark_in_index = frame_start - 1 - mark_out_index = frame_end - 1 - - tmp_filename_template = "pos_{}." + filename_template - - files_by_position = {} - for position in sorted_positions: - layer = layers_by_position[position] - behavior = behavior_by_layer_id[layer["layer_id"]] - files_by_frames = self.render_layer( - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ) - files_by_position[position] = files_by_frames - - output = self._composite_files( - files_by_position, + first_frame_filepath = os.path.join( output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height + filename_template.format(frame_start, frame=frame_start) ) - self._cleanup_tmp_files(files_by_position) - return output + mark_in = frame_start - 1 + mark_out = frame_end - 1 - def render_layer( - self, - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ): - layer_id = layer["layer_id"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - exposure_frames = lib.get_exposure_frames( - layer_id, frame_start_index, frame_end_index - ) - if frame_start_index not in exposure_frames: - exposure_frames.append(frame_start_index) - - layer_files_by_frame = {} george_script_lines = [ - "tv_SaveMode \"PNG\"" + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) ] - layer_position = layer["position"] - - for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) - dst_path = "/".join([output_dir, filename]) - layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame_idx)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id - )) - # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) - # Fill frames between `frame_start_index` and `frame_end_index` - self.log.debug(( - "Filling frames between first and last frame of layer ({} - {})." - ).format(frame_start_index + 1, frame_end_index + 1)) - - prev_filepath = None - for frame_idx in range(frame_start_index, frame_end_index + 1): - if frame_idx in layer_files_by_frame: - prev_filepath = layer_files_by_frame[frame_idx] - continue - - if prev_filepath is None: - raise ValueError("BUG: First frame of layer was not rendered!") - - filename = tmp_filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(prev_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - # Fill frames by pre/post behavior of layer - pre_behavior = behavior["pre"] - post_behavior = behavior["post"] - self.log.debug(( - "Completing image sequence of layer by pre/post behavior." - " PRE: {} | POST: {}" - ).format(pre_behavior, post_behavior)) - - # Pre behavior - self._fill_frame_by_pre_behavior( - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - self._fill_frame_by_post_behavior( - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - return layer_files_by_frame - - def _fill_frame_by_pre_behavior( - self, - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_in_index >= frame_start_index: - return - - if pre_behavior == "none": - return - - if pre_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_start_index] - for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = ( - (frame_end_index - frame_idx) % frame_count - ) - eq_frame_idx = frame_end_index - eq_frame_idx_offset - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) - eq_frame_idx = frame_start_index + eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _fill_frame_by_post_behavior( - self, - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_out_index <= frame_end_index: - return - - if post_behavior == "none": - return - - if post_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_end_index] - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx = frame_idx % frame_count - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = seq_len - eq_frame_idx_offset - eq_frame_idx = frame_end_index - eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height - ): - # Prepare paths to images by frames into list where are stored - # in order of compositing. - images_by_frame = {} - for frame_idx in range(frame_start, frame_end + 1): - images_by_frame[frame_idx] = [] - for position in sorted(files_by_position.keys(), reverse=True): - position_data = files_by_position[position] - if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) - - process_count = os.cpu_count() - if process_count > 1: - process_count -= 1 - - processes = {} - output_filepaths = [] - thumbnail_src_filepath = None - for frame_idx in sorted(images_by_frame.keys()): - image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame) - output_filepath = os.path.join(output_dir, output_filename) - output_filepaths.append(output_filepath) - - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height - ) + output = [] + for frame in range(frame_start, frame_end + 1): + output.append( + filename_template.format(frame, frame=frame) ) - - # Wait until all processes are done - running_processes = {} - while True: - for idx in tuple(running_processes.keys()): - process = running_processes[idx] - if not process.is_alive(): - running_processes.pop(idx).join() - - if processes and len(running_processes) != process_count: - indexes = list(processes.keys()) - for _ in range(process_count - len(running_processes)): - if not indexes: - break - idx = indexes.pop(0) - running_processes[idx] = processes.pop(idx) - running_processes[idx].start() - - if not running_processes and not processes: - break - - time.sleep(0.01) - - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath - - def _cleanup_tmp_files(self, files_by_position): - for data in files_by_position.values(): - for filepath in data.values(): - os.remove(filepath) - - def _copy_image(self, src_path, dst_path): - # Create hardlink of image instead of copying if possible - if hasattr(os, "link"): - os.link(src_path, dst_path) - else: - shutil.copy(src_path, dst_path) + return output From 9c28c48b7138b139ac2bb631b9ea02c54f95c5b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:42:21 +0100 Subject: [PATCH 028/100] removed unused lib --- pype/hosts/tvpaint/lib.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py deleted file mode 100644 index 8172392c7f..0000000000 --- a/pype/hosts/tvpaint/lib.py +++ /dev/null @@ -1,17 +0,0 @@ -from PIL import Image - - -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) From 9e6b8b0030199bc45f7384caca1345a6aa663a58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:50:22 +0100 Subject: [PATCH 029/100] fixed hound --- .../hosts/tvpaint/plugins/publish/collect_workfile_data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index f25e274581..bb25e244ef 100644 --- a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -113,7 +113,8 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - _frame_start = int(workfile_info_parts.pop(-1)) + # Project frame start - not used + workfile_info_parts.pop(-1) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -141,8 +142,8 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): clip_info_str = lib.execute_george("tv_clipinfo") self.log.debug("Clip info: {}".format(clip_info_str)) clip_info_items = clip_info_str.split(" ") - # Color index - color_idx = clip_info_items.pop(-1) + # Color index - not used + clip_info_items.pop(-1) clip_info_items.pop(-1) mark_out = int(clip_info_items.pop(-1)) + 1 From f1058f98f5a1b2212b5c17800fb58e1580e06bae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:57:02 +0100 Subject: [PATCH 030/100] fix variable names --- .../tvpaint/plugins/publish/collect_workfile_data.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index bb25e244ef..7965112136 100644 --- a/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/pype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -146,10 +146,12 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): clip_info_items.pop(-1) clip_info_items.pop(-1) - mark_out = int(clip_info_items.pop(-1)) + 1 + mark_out = int(clip_info_items.pop(-1)) + frame_end = mark_out + 1 clip_info_items.pop(-1) - mark_in = int(clip_info_items.pop(-1)) + 1 + mark_in = int(clip_info_items.pop(-1)) + frame_start = mark_in + 1 clip_info_items.pop(-1) - return mark_in, mark_out + return frame_start, frame_end From f166dee723b9acdef67c3857128239cdd0a07000 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 15:26:35 +0100 Subject: [PATCH 031/100] fix maya playblast settings --- pype/hosts/maya/api/lib.py | 49 +++-- .../maya/plugins/publish/extract_playblast.py | 10 +- .../defaults/project_settings/maya.json | 29 +-- .../schemas/template_maya_capture.json | 167 +++++++----------- 4 files changed, 94 insertions(+), 161 deletions(-) diff --git a/pype/hosts/maya/api/lib.py b/pype/hosts/maya/api/lib.py index dc802b6a37..56621949b3 100644 --- a/pype/hosts/maya/api/lib.py +++ b/pype/hosts/maya/api/lib.py @@ -2127,15 +2127,9 @@ def bake_to_world_space(nodes, def load_capture_preset(path=None, data=None): - import capture_gui import capture - if data: - preset = data - else: - path = path - preset = capture_gui.lib.load_json(path) - print(preset) + preset = data options = dict() @@ -2177,29 +2171,27 @@ def load_capture_preset(path=None, data=None): temp_options2 = {} id = 'Viewport Options' - light_options = { - 0: "default", - 1: 'all', - 2: 'selected', - 3: 'flat', - 4: 'nolights'} for key in preset[id]: - if key == 'high_quality': - if preset[id][key] == True: - temp_options2['multiSampleEnable'] = True - temp_options2['multiSampleCount'] = 4 - temp_options2['textureMaxResolution'] = 1024 + if key == 'textureMaxResolution': + if preset[id][key] > 0: + temp_options2['textureMaxResolution'] = preset[id][key] temp_options2['enableTextureMaxRes'] = True temp_options2['textureMaxResMode'] = 1 else: - temp_options2['multiSampleEnable'] = False - temp_options2['multiSampleCount'] = 4 - temp_options2['textureMaxResolution'] = 512 - temp_options2['enableTextureMaxRes'] = True + temp_options2['textureMaxResolution'] = preset[id][key] + temp_options2['enableTextureMaxRes'] = False temp_options2['textureMaxResMode'] = 0 + if key == 'multiSample': + if preset[id][key] > 0: + temp_options2['multiSampleEnable'] = True + temp_options2['multiSampleCount'] = preset[id][key] + else: + temp_options2['multiSampleEnable'] = False + temp_options2['multiSampleCount'] = preset[id][key] + if key == 'ssaoEnable': - if preset[id][key] == True: + if preset[id][key] is True: temp_options2['ssaoEnable'] = True else: temp_options2['ssaoEnable'] = False @@ -2211,18 +2203,17 @@ def load_capture_preset(path=None, data=None): if key == 'headsUpDisplay': temp_options['headsUpDisplay'] = True - if key == 'displayLights': - temp_options[str(key)] = light_options[preset[id][key]] else: temp_options[str(key)] = preset[id][key] for key in ['override_viewport_options', 'high_quality', 'alphaCut', - 'gpuCacheDisplayFilter']: - temp_options.pop(key, None) - - for key in ['ssaoEnable']: + 'gpuCacheDisplayFilter', + 'multiSample', + 'ssaoEnable', + 'textureMaxResolution' + ]: temp_options.pop(key, None) options['viewport_options'] = temp_options diff --git a/pype/hosts/maya/plugins/publish/extract_playblast.py b/pype/hosts/maya/plugins/publish/extract_playblast.py index da1428400e..99411e7f53 100644 --- a/pype/hosts/maya/plugins/publish/extract_playblast.py +++ b/pype/hosts/maya/plugins/publish/extract_playblast.py @@ -45,11 +45,8 @@ class ExtractPlayblast(pype.api.Extractor): # get cameras camera = instance.data['review_camera'] - try: - preset = lib.load_capture_preset(data=self.capture_preset) - except Exception: - preset = {} - self.log.info('using viewport preset: {}'.format(preset)) + preset = lib.load_capture_preset(data=self.capture_preset) + preset['camera'] = camera preset['format'] = "image" @@ -99,6 +96,9 @@ class ExtractPlayblast(pype.api.Extractor): # Remove panel key since it's internal value to capture_gui preset.pop("panel", None) + + self.log.info('using viewport preset: {}'.format(preset)) + path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index c1f43ff81e..75aa32fb36 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -284,13 +284,6 @@ "isolate_view": true, "off_screen": true }, - "IO": { - "name": "", - "open_finished": true, - "raw_frame_numbers": true, - "recent_playblasts": [], - "save_file": true - }, "PanZoom": { "pan_zoom": true }, @@ -303,19 +296,20 @@ "percent": 1.0, "mode": "Custom" }, - "Time Range": { - "start_frame": 0, - "end_frame": 0, - "frame": "", - "time": "Time Slider" - }, "Viewport Options": { + "override_viewport_options": true, + "displayLights": "0", + "textureMaxResolution": 1024, + "multiSample": 4, + "shadows": true, + "textures": true, + "twoSidedLighting": true, + "ssaoEnable": true, "cameras": false, "clipGhosts": false, "controlVertices": false, "deformers": false, "dimensions": false, - "displayLights": 0, "dynamicConstraints": false, "dynamics": false, "fluids": false, @@ -325,7 +319,6 @@ "grid": false, "hairSystems": true, "handles": false, - "high_quality": true, "hud": false, "hulls": false, "ikHandles": false, @@ -340,17 +333,13 @@ "nRigids": false, "nurbsCurves": false, "nurbsSurfaces": false, - "override_viewport_options": true, "particleInstancers": false, "pivots": false, "planes": false, "pluginShapes": false, "polymeshes": true, - "shadows": true, "strokes": false, - "subdivSurfaces": false, - "textures": false, - "twoSidedLighting": true + "subdivSurfaces": false }, "Camera Options": { "displayGateMask": false, diff --git a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json index f6fcb3b998..e4e0b034dd 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json @@ -163,42 +163,7 @@ } ] }, - { - "type": "dict", - "key": "IO", - "children": [ - { - "type": "label", - "label": "IO" - }, - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "open_finished", - "label": "Open finished" - }, - { - "type": "boolean", - "key": "raw_frame_numbers", - "label": "Raw frame numbers" - }, - { - "type": "list", - "key": "recent_playblasts", - "label": "Recent Playblasts", - "object_type": "text" - }, - { - "type": "boolean", - "key": "save_file", - "label": "Save file" - } - ] - }, + { "type": "dict", "key": "PanZoom", @@ -222,9 +187,12 @@ "label": "Renderer" }, { - "type": "text", + "type": "enum", "key": "rendererName", - "label": " Renderer name" + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] } ] }, @@ -273,48 +241,66 @@ { "type": "splitter" }, - { - "type": "dict", - "key": "Time Range", - "children": [ - { - "type": "label", - "label": "Time Range" - }, - { - "type": "number", - "key": "start_frame", - "label": " Start frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "number", - "key": "end_frame", - "label": "End frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "text", - "key": "frame", - "label": "Frame" - }, - { - "type": "text", - "key": "time", - "label": "Time" - } - ] - }, { "type": "dict", "collapsible": true, "key": "Viewport Options", "label": "Viewport Options", "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "override_viewport_options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "textures", + "label": "Display Textures" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "splitter" + }, { "type": "boolean", "key": "cameras", @@ -340,14 +326,6 @@ "key": "dimensions", "label": "dimensions" }, - { - "type": "number", - "key": "displayLights", - "label": "displayLights", - "decimal": 0, - "minimum": 0, - "maximum": 10 - }, { "type": "boolean", "key": "dynamicConstraints", @@ -393,11 +371,6 @@ "key": "handles", "label": "handles" }, - { - "type": "boolean", - "key": "high_quality", - "label": "high_quality" - }, { "type": "boolean", "key": "hud", @@ -468,11 +441,6 @@ "key": "nurbsSurfaces", "label": "nurbsSurfaces" }, - { - "type": "boolean", - "key": "override_viewport_options", - "label": "override_viewport_options" - }, { "type": "boolean", "key": "particleInstancers", @@ -498,11 +466,6 @@ "key": "polymeshes", "label": "polymeshes" }, - { - "type": "boolean", - "key": "shadows", - "label": "shadows" - }, { "type": "boolean", "key": "strokes", @@ -512,16 +475,6 @@ "type": "boolean", "key": "subdivSurfaces", "label": "subdivSurfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "textures" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "twoSidedLighting" } ] }, From 325206a4336766be0b9cc18491095c028eab054b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 15:07:46 +0100 Subject: [PATCH 032/100] kept only one integrate hierarchy ftrack plugin --- .../publish/integrate_hierarchy_ftrack.py | 52 ++- .../publish/integrate_hierarchy_ftrack_SP.py | 331 ------------------ 2 files changed, 42 insertions(+), 341 deletions(-) delete mode 100644 pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack_SP.py diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index c4f7726071..ac606ed27d 100644 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,12 +1,13 @@ import sys import six +import collections import pyblish.api from avalon import io -try: - from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC -except Exception: - CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" +from pype.modules.ftrack.lib.avalon_sync import ( + CUST_ATTR_AUTO_SYNC, + get_pype_attr +) class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): @@ -36,7 +37,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["hiero", "resolve"] + hosts = ["standalonepublisher"] optional = False def process(self, context): @@ -74,6 +75,15 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.auto_sync_on(project) def import_to_ftrack(self, input_data, parent=None): + # Prequery hiearchical custom attributes + hier_custom_attributes = get_pype_attr(self.session)[1] + hier_attr_by_key = { + attr["key"]: attr + for attr in hier_custom_attributes + } + # Get ftrack api module (as they are different per python version) + ftrack_api = self.context.data["ftrackPythonModule"] + for entity_name in input_data: entity_data = input_data[entity_name] entity_type = entity_data['entity_type'] @@ -116,12 +126,34 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): i for i in self.context if i.data['asset'] in entity['name'] ] for key in custom_attributes: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) - ) + hier_attr = hier_attr_by_key.get(key) + # Use simple method if key is not hierarchical + if not hier_attr: + assert (key in entity['custom_attributes']), ( + 'Missing custom attribute key: `{0}` in attrs: ' + '`{1}`'.format(key, entity['custom_attributes'].keys()) + ) - entity['custom_attributes'][key] = custom_attributes[key] + entity['custom_attributes'][key] = custom_attributes[key] + + else: + # Use ftrack operations method to set hiearchical + # attribute value. + # - this is because there may be non hiearchical custom + # attributes with different properties + entity_key = collections.OrderedDict({ + "configuration_id": hier_attr["id"], + "entity_id": entity["id"] + }) + self.session.recorded_operations.push( + ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + custom_attributes[key] + ) + ) for instance in instances: instance.data['ftrackEntity'] = entity diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack_SP.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack_SP.py deleted file mode 100644 index ac606ed27d..0000000000 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack_SP.py +++ /dev/null @@ -1,331 +0,0 @@ -import sys -import six -import collections -import pyblish.api -from avalon import io - -from pype.modules.ftrack.lib.avalon_sync import ( - CUST_ATTR_AUTO_SYNC, - get_pype_attr -) - - -class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): - """ - Create entities in ftrack based on collected data from premiere - Example of entry data: - { - "ProjectXS": { - "entity_type": "Project", - "custom_attributes": { - "fps": 24,... - }, - "tasks": [ - "Compositing", - "Lighting",... *task must exist as task type in project schema* - ], - "childs": { - "sq01": { - "entity_type": "Sequence", - ... - } - } - } - } - """ - - order = pyblish.api.IntegratorOrder - 0.04 - label = 'Integrate Hierarchy To Ftrack' - families = ["shot"] - hosts = ["standalonepublisher"] - optional = False - - def process(self, context): - self.context = context - if "hierarchyContext" not in self.context.data: - return - - hierarchy_context = self.context.data["hierarchyContext"] - - self.session = self.context.data["ftrackSession"] - project_name = self.context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] - - if not io.Session: - io.install() - - self.ft_project = None - - input_data = hierarchy_context - - # disable termporarily ftrack project's autosyncing - if auto_sync_state: - self.auto_sync_off(project) - - try: - # import ftrack hierarchy - self.import_to_ftrack(input_data) - except Exception: - raise - finally: - if auto_sync_state: - self.auto_sync_on(project) - - def import_to_ftrack(self, input_data, parent=None): - # Prequery hiearchical custom attributes - hier_custom_attributes = get_pype_attr(self.session)[1] - hier_attr_by_key = { - attr["key"]: attr - for attr in hier_custom_attributes - } - # Get ftrack api module (as they are different per python version) - ftrack_api = self.context.data["ftrackPythonModule"] - - for entity_name in input_data: - entity_data = input_data[entity_name] - entity_type = entity_data['entity_type'] - self.log.debug(entity_data) - self.log.debug(entity_type) - - if entity_type.lower() == 'project': - query = 'Project where full_name is "{}"'.format(entity_name) - entity = self.session.query(query).one() - self.ft_project = entity - self.task_types = self.get_all_task_types(entity) - - elif self.ft_project is None or parent is None: - raise AssertionError( - "Collected items are not in right order!" - ) - - # try to find if entity already exists - else: - query = ( - 'TypedContext where name is "{0}" and ' - 'project_id is "{1}"' - ).format(entity_name, self.ft_project["id"]) - try: - entity = self.session.query(query).one() - except Exception: - entity = None - - # Create entity if not exists - if entity is None: - entity = self.create_entity( - name=entity_name, - type=entity_type, - parent=parent - ) - # self.log.info('entity: {}'.format(dict(entity))) - # CUSTOM ATTRIBUTES - custom_attributes = entity_data.get('custom_attributes', []) - instances = [ - i for i in self.context if i.data['asset'] in entity['name'] - ] - for key in custom_attributes: - hier_attr = hier_attr_by_key.get(key) - # Use simple method if key is not hierarchical - if not hier_attr: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) - ) - - entity['custom_attributes'][key] = custom_attributes[key] - - else: - # Use ftrack operations method to set hiearchical - # attribute value. - # - this is because there may be non hiearchical custom - # attributes with different properties - entity_key = collections.OrderedDict({ - "configuration_id": hier_attr["id"], - "entity_id": entity["id"] - }) - self.session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - custom_attributes[key] - ) - ) - - for instance in instances: - instance.data['ftrackEntity'] = entity - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # TASKS - tasks = entity_data.get('tasks', []) - existing_tasks = [] - tasks_to_create = [] - for child in entity['children']: - if child.entity_type.lower() == 'task': - existing_tasks.append(child['name'].lower()) - # existing_tasks.append(child['type']['name']) - - for task_name in tasks: - task_type = tasks[task_name]["type"] - if task_name.lower() in existing_tasks: - print("Task {} already exists".format(task_name)) - continue - tasks_to_create.append((task_name, task_type)) - - for task_name, task_type in tasks_to_create: - self.create_task( - name=task_name, - task_type=task_type, - parent=entity - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Incoming links. - self.create_links(entity_data, entity) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Create notes. - user = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).first() - if user: - for comment in entity_data.get("comments", []): - entity.create_note(comment, user) - else: - self.log.warning( - "Was not able to query current User {}".format( - self.session.api_user - ) - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Import children. - if 'childs' in entity_data: - self.import_to_ftrack( - entity_data['childs'], entity) - - def create_links(self, entity_data, entity): - # Clear existing links. - for link in entity.get("incoming_links", []): - self.session.delete(link) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Create new links. - for input in entity_data.get("inputs", []): - input_id = io.find_one({"_id": input})["data"]["ftrackId"] - assetbuild = self.session.get("AssetBuild", input_id) - self.log.debug( - "Creating link from {0} to {1}".format( - assetbuild["name"], entity["name"] - ) - ) - self.session.create( - "TypedContextLink", {"from": assetbuild, "to": entity} - ) - - def get_all_task_types(self, project): - tasks = {} - proj_template = project['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - - def create_task(self, name, task_type, parent): - task = self.session.create('Task', { - 'name': name, - 'parent': parent - }) - # TODO not secured!!! - check if task_type exists - self.log.info(task_type) - self.log.info(self.task_types) - task['type'] = self.task_types[task_type] - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return task - - def create_entity(self, name, type, parent): - entity = self.session.create(type, { - 'name': name, - 'parent': parent - }) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return entity - - def auto_sync_off(self, project): - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - - self.log.info("Ftrack autosync swithed off") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def auto_sync_on(self, project): - - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - self.log.info("Ftrack autosync swithed on") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) From b68a40cdffafc4b4e44ad99b2782707c20a9b950 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 15:38:20 +0100 Subject: [PATCH 033/100] copied code and constant as it is not possible to import pype's ftrack module in python 2 --- .../publish/integrate_hierarchy_ftrack.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index ac606ed27d..940aee6f74 100644 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,13 +1,36 @@ import sys -import six import collections +import six import pyblish.api from avalon import io -from pype.modules.ftrack.lib.avalon_sync import ( - CUST_ATTR_AUTO_SYNC, - get_pype_attr -) +# Copy of constant `pype.modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + + +# Copy of `get_pype_attr` from pype.modules.ftrack.lib +def get_pype_attr(session, split_hierarchical=True): + custom_attributes = [] + hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query + cust_attrs_query = ( + "select id, entity_type, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where group.name in (\"avalon\", \"pype\")" + ) + all_avalon_attr = session.query(cust_attrs_query).all() + for cust_attr in all_avalon_attr: + if split_hierarchical and cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue + + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + + return custom_attributes class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): From 55c0d83dec29b137a01d723fd1530e49a6bbb68d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 16:38:37 +0100 Subject: [PATCH 034/100] added hiero to hosts --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 940aee6f74..c4c311bf44 100644 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -60,7 +60,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["standalonepublisher"] + hosts = ["hiero", "standalonepublisher"] optional = False def process(self, context): From 0479bf451347eb980eb4b7522b20a8a400d56745 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 16:39:26 +0100 Subject: [PATCH 035/100] added resolve to hosts --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index c4c311bf44..56c317ef3d 100644 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -60,7 +60,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["hiero", "standalonepublisher"] + hosts = ["hiero", "resolve", "standalonepublisher"] optional = False def process(self, context): From feaaa387c0938745bdff25a3a2801f61cd33e047 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 17:29:00 +0100 Subject: [PATCH 036/100] copied maya look asigner to pype tools --- pype/tools/mayalookassigner/LICENSE | 21 ++ pype/tools/mayalookassigner/README.MD | 10 + pype/tools/mayalookassigner/__init__.py | 9 + pype/tools/mayalookassigner/app.py | 248 ++++++++++++++++++++++ pype/tools/mayalookassigner/commands.py | 194 ++++++++++++++++++ pype/tools/mayalookassigner/models.py | 120 +++++++++++ pype/tools/mayalookassigner/views.py | 50 +++++ pype/tools/mayalookassigner/widgets.py | 261 ++++++++++++++++++++++++ 8 files changed, 913 insertions(+) create mode 100644 pype/tools/mayalookassigner/LICENSE create mode 100644 pype/tools/mayalookassigner/README.MD create mode 100644 pype/tools/mayalookassigner/__init__.py create mode 100644 pype/tools/mayalookassigner/app.py create mode 100644 pype/tools/mayalookassigner/commands.py create mode 100644 pype/tools/mayalookassigner/models.py create mode 100644 pype/tools/mayalookassigner/views.py create mode 100644 pype/tools/mayalookassigner/widgets.py diff --git a/pype/tools/mayalookassigner/LICENSE b/pype/tools/mayalookassigner/LICENSE new file mode 100644 index 0000000000..852751dbe4 --- /dev/null +++ b/pype/tools/mayalookassigner/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Colorbleed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pype/tools/mayalookassigner/README.MD b/pype/tools/mayalookassigner/README.MD new file mode 100644 index 0000000000..cfd0c15d89 --- /dev/null +++ b/pype/tools/mayalookassigner/README.MD @@ -0,0 +1,10 @@ +# Maya Look Assigner + +Tool to assign published lookdev shaders to selected or all objects. +Each shader variation is listed based on the unique asset Id of the +selected or all objects. + +## Dependencies +* Avalon +* Colorbleed configuration for Avalon +* Autodesk Maya 2016 and up diff --git a/pype/tools/mayalookassigner/__init__.py b/pype/tools/mayalookassigner/__init__.py new file mode 100644 index 0000000000..616a3e94d0 --- /dev/null +++ b/pype/tools/mayalookassigner/__init__.py @@ -0,0 +1,9 @@ +from .app import ( + App, + show +) + + +__all__ = [ + "App", + "show"] diff --git a/pype/tools/mayalookassigner/app.py b/pype/tools/mayalookassigner/app.py new file mode 100644 index 0000000000..eacf373052 --- /dev/null +++ b/pype/tools/mayalookassigner/app.py @@ -0,0 +1,248 @@ +import sys +import time +import logging + +import pype.hosts.maya.lib as cblib + +from avalon import style, io +from avalon.tools import lib +from avalon.vendor.Qt import QtWidgets, QtCore + +from maya import cmds +# old api for MFileIO +import maya.OpenMaya +import maya.api.OpenMaya as om + +from . import widgets +from . import commands + +module = sys.modules[__name__] +module.window = None + + +class App(QtWidgets.QWidget): + + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent=parent) + + self.log = logging.getLogger(__name__) + + # Store callback references + self._callbacks = [] + + filename = commands.get_workfile() + + self.setObjectName("lookManager") + self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename)) + self.setWindowFlags(QtCore.Qt.Window) + self.setParent(parent) + + # Force to delete the window on close so it triggers + # closeEvent only once. Otherwise it's retriggered when + # the widget gets garbage collected. + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + self.resize(750, 500) + + self.setup_ui() + + self.setup_connections() + + # Force refresh check on initialization + self._on_renderlayer_switch() + + def setup_ui(self): + """Build the UI""" + + # Assets (left) + asset_outliner = widgets.AssetOutliner() + + # Looks (right) + looks_widget = QtWidgets.QWidget() + looks_layout = QtWidgets.QVBoxLayout(looks_widget) + + look_outliner = widgets.LookOutliner() # Database look overview + + assign_selected = QtWidgets.QCheckBox("Assign to selected only") + assign_selected.setToolTip("Whether to assign only to selected nodes " + "or to the full asset") + remove_unused_btn = QtWidgets.QPushButton("Remove Unused Looks") + + looks_layout.addWidget(look_outliner) + looks_layout.addWidget(assign_selected) + looks_layout.addWidget(remove_unused_btn) + + # Footer + status = QtWidgets.QStatusBar() + status.setSizeGripEnabled(False) + status.setFixedHeight(25) + warn_layer = QtWidgets.QLabel("Current Layer is not " + "defaultRenderLayer") + warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + warn_layer.setStyleSheet("color: #DD5555; font-weight: bold;") + warn_layer.setFixedHeight(25) + + footer = QtWidgets.QHBoxLayout() + footer.setContentsMargins(0, 0, 0, 0) + footer.addWidget(status) + footer.addWidget(warn_layer) + + # Build up widgets + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setSpacing(0) + main_splitter = QtWidgets.QSplitter() + main_splitter.setStyleSheet("QSplitter{ border: 0px; }") + main_splitter.addWidget(asset_outliner) + main_splitter.addWidget(looks_widget) + main_splitter.setSizes([350, 200]) + main_layout.addWidget(main_splitter) + main_layout.addLayout(footer) + + # Set column width + asset_outliner.view.setColumnWidth(0, 200) + look_outliner.view.setColumnWidth(0, 150) + + # Open widgets + self.asset_outliner = asset_outliner + self.look_outliner = look_outliner + self.status = status + self.warn_layer = warn_layer + + # Buttons + self.remove_unused = remove_unused_btn + self.assign_selected = assign_selected + + def setup_connections(self): + """Connect interactive widgets with actions""" + + self.asset_outliner.selection_changed.connect( + self.on_asset_selection_changed) + + self.asset_outliner.refreshed.connect( + lambda: self.echo("Loaded assets..")) + + self.look_outliner.menu_apply_action.connect(self.on_process_selected) + self.remove_unused.clicked.connect(commands.remove_unused_looks) + + # Maya renderlayer switch callback + callback = om.MEventMessage.addEventCallback( + "renderLayerManagerChange", + self._on_renderlayer_switch + ) + self._callbacks.append(callback) + + def closeEvent(self, event): + + # Delete callbacks + for callback in self._callbacks: + om.MMessage.removeCallback(callback) + + return super(App, self).closeEvent(event) + + def _on_renderlayer_switch(self, *args): + """Callback that updates on Maya renderlayer switch""" + + if maya.OpenMaya.MFileIO.isNewingFile(): + # Don't perform a check during file open or file new as + # the renderlayers will not be in a valid state yet. + return + + layer = cmds.editRenderLayerGlobals(query=True, + currentRenderLayer=True) + if layer != "defaultRenderLayer": + self.warn_layer.show() + else: + self.warn_layer.hide() + + def echo(self, message): + self.status.showMessage(message, 1500) + + def refresh(self): + """Refresh the content""" + + # Get all containers and information + self.asset_outliner.clear() + found_items = self.asset_outliner.get_all_assets() + if not found_items: + self.look_outliner.clear() + + def on_asset_selection_changed(self): + """Get selected items from asset loader and fill look outliner""" + + items = self.asset_outliner.get_selected_items() + self.look_outliner.clear() + self.look_outliner.add_items(items) + + def on_process_selected(self): + """Process all selected looks for the selected assets""" + + assets = self.asset_outliner.get_selected_items() + assert assets, "No asset selected" + + # Collect the looks we want to apply (by name) + look_items = self.look_outliner.get_selected_items() + looks = {look["subset"] for look in look_items} + + selection = self.assign_selected.isChecked() + asset_nodes = self.asset_outliner.get_nodes(selection=selection) + + start = time.time() + for i, (asset, item) in enumerate(asset_nodes.items()): + + # Label prefix + prefix = "({}/{})".format(i+1, len(asset_nodes)) + + # Assign the first matching look relevant for this asset + # (since assigning multiple to the same nodes makes no sense) + assign_look = next((subset for subset in item["looks"] + if subset["name"] in looks), None) + if not assign_look: + self.echo("{} No matching selected " + "look for {}".format(prefix, asset)) + continue + + # Get the latest version of this asset's look subset + version = io.find_one({"type": "version", + "parent": assign_look["_id"]}, + sort=[("name", -1)]) + + subset_name = assign_look["name"] + self.echo("{} Assigning {} to {}\t".format(prefix, + subset_name, + asset)) + + # Assign look + cblib.assign_look_by_version(nodes=item["nodes"], + version_id=version["_id"]) + + end = time.time() + + self.echo("Finished assigning.. ({0:.3f}s)".format(end - start)) + + +def show(): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + # Get Maya main window + top_level_widgets = QtWidgets.QApplication.topLevelWidgets() + mainwindow = next(widget for widget in top_level_widgets + if widget.objectName() == "MayaWindow") + + with lib.application(): + window = App(parent=mainwindow) + window.setStyleSheet(style.load_stylesheet()) + window.show() + + module.window = window diff --git a/pype/tools/mayalookassigner/commands.py b/pype/tools/mayalookassigner/commands.py new file mode 100644 index 0000000000..d1235bf8c4 --- /dev/null +++ b/pype/tools/mayalookassigner/commands.py @@ -0,0 +1,194 @@ +from collections import defaultdict +import logging +import os + +import maya.cmds as cmds + +try: + import pype.maya.lib as cblib +except Exception: + import pype.hosts.maya.lib as cblib + +from avalon import io, api + +log = logging.getLogger(__name__) + + +def get_workfile(): + path = cmds.file(query=True, sceneName=True) or "untitled" + return os.path.basename(path) + + +def get_workfolder(): + return os.path.dirname(cmds.file(query=True, sceneName=True)) + + +def select(nodes): + cmds.select(nodes) + + +def get_namespace_from_node(node): + """Get the namespace from the given node + + Args: + node (str): name of the node + + Returns: + namespace (str) + + """ + parts = node.rsplit("|", 1)[-1].rsplit(":", 1) + return parts[0] if len(parts) > 1 else u":" + + +def list_descendents(nodes): + """Include full descendant hierarchy of given nodes. + + This is a workaround to cmds.listRelatives(allDescendents=True) because + this way correctly keeps children instance paths (see Maya documentation) + + This fixes LKD-26: assignments not working as expected on instanced shapes. + + Return: + list: List of children descendents of nodes + + """ + result = [] + while True: + nodes = cmds.listRelatives(nodes, + fullPath=True) + if nodes: + result.extend(nodes) + else: + return result + + +def get_selected_nodes(): + """Get information from current selection""" + + selection = cmds.ls(selection=True, long=True) + hierarchy = list_descendents(selection) + nodes = list(set(selection + hierarchy)) + + return nodes + + +def get_all_asset_nodes(): + """Get all assets from the scene, container based + + Returns: + list: list of dictionaries + """ + + host = api.registered_host() + + nodes = [] + for container in host.ls(): + # We are not interested in looks but assets! + if container["loader"] == "LookLoader": + continue + + # Gather all information + container_name = container["objectName"] + nodes += cmds.sets(container_name, query=True, nodesOnly=True) or [] + + return nodes + + +def create_asset_id_hash(nodes): + """Create a hash based on cbId attribute value + Args: + nodes (list): a list of nodes + + Returns: + dict + """ + node_id_hash = defaultdict(list) + for node in nodes: + value = cblib.get_id(node) + if value is None: + continue + + asset_id = value.split(":")[0] + node_id_hash[asset_id].append(node) + + return dict(node_id_hash) + + +def create_items_from_nodes(nodes): + """Create an item for the view based the container and content of it + + It fetches the look document based on the asset ID found in the content. + The item will contain all important information for the tool to work. + + If there is an asset ID which is not registered in the project's collection + it will log a warning message. + + Args: + nodes (list): list of maya nodes + + Returns: + list of dicts + + """ + + asset_view_items = [] + + id_hashes = create_asset_id_hash(nodes) + if not id_hashes: + return asset_view_items + + for _id, id_nodes in id_hashes.items(): + asset = io.find_one({"_id": io.ObjectId(_id)}, + projection={"name": True}) + + # Skip if asset id is not found + if not asset: + log.warning("Id not found in the database, skipping '%s'." % _id) + log.warning("Nodes: %s" % id_nodes) + continue + + # Collect available look subsets for this asset + looks = cblib.list_looks(asset["_id"]) + + # Collect namespaces the asset is found in + namespaces = set() + for node in id_nodes: + namespace = get_namespace_from_node(node) + namespaces.add(namespace) + + asset_view_items.append({"label": asset["name"], + "asset": asset, + "looks": looks, + "namespaces": namespaces}) + + return asset_view_items + + +def remove_unused_looks(): + """Removes all loaded looks for which none of the shaders are used. + + This will cleanup all loaded "LookLoader" containers that are unused in + the current scene. + + """ + + host = api.registered_host() + + unused = list() + for container in host.ls(): + if container['loader'] == "LookLoader": + members = cmds.sets(container['objectName'], query=True) + look_sets = cmds.ls(members, type="objectSet") + for look_set in look_sets: + # If the set is used than we consider this look *in use* + if cmds.sets(look_set, query=True): + break + else: + unused.append(container) + + for container in unused: + log.info("Removing unused look container: %s", container['objectName']) + api.remove(container) + + log.info("Finished removing unused looks. (see log for details)") diff --git a/pype/tools/mayalookassigner/models.py b/pype/tools/mayalookassigner/models.py new file mode 100644 index 0000000000..7c5133de82 --- /dev/null +++ b/pype/tools/mayalookassigner/models.py @@ -0,0 +1,120 @@ +from collections import defaultdict +from avalon.tools import models + +from avalon.vendor.Qt import QtCore +from avalon.vendor import qtawesome +from avalon.style import colors + + +class AssetModel(models.TreeModel): + + Columns = ["label"] + + def add_items(self, items): + """ + Add items to model with needed data + Args: + items(list): collection of item data + + Returns: + None + """ + + self.beginResetModel() + + # Add the items sorted by label + sorter = lambda x: x["label"] + + for item in sorted(items, key=sorter): + + asset_item = models.Item() + asset_item.update(item) + asset_item["icon"] = "folder" + + # Add namespace children + namespaces = item["namespaces"] + for namespace in sorted(namespaces): + child = models.Item() + child.update(item) + child.update({ + "label": (namespace if namespace != ":" + else "(no namespace)"), + "namespace": namespace, + "looks": item["looks"], + "icon": "folder-o" + }) + asset_item.add_child(child) + + self.add_child(asset_item) + + self.endResetModel() + + def data(self, index, role): + + if not index.isValid(): + return + + if role == models.TreeModel.ItemRole: + node = index.internalPointer() + return node + + # Add icon + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + node = index.internalPointer() + icon = node.get("icon") + if icon: + return qtawesome.icon("fa.{0}".format(icon), + color=colors.default) + + return super(AssetModel, self).data(index, role) + + +class LookModel(models.TreeModel): + """Model displaying a list of looks and matches for assets""" + + Columns = ["label", "match"] + + def add_items(self, items): + """Add items to model with needed data + + An item exists of: + { + "subset": 'name of subset', + "asset": asset_document + } + + Args: + items(list): collection of item data + + Returns: + None + """ + + self.beginResetModel() + + # Collect the assets per look name (from the items of the AssetModel) + look_subsets = defaultdict(list) + for asset_item in items: + asset = asset_item["asset"] + for look in asset_item["looks"]: + look_subsets[look["name"]].append(asset) + + for subset, assets in sorted(look_subsets.iteritems()): + + # Define nice label without "look" prefix for readability + label = subset if not subset.startswith("look") else subset[4:] + + item_node = models.Item() + item_node["label"] = label + item_node["subset"] = subset + + # Amount of matching assets for this look + item_node["match"] = len(assets) + + # Store the assets that have this subset available + item_node["assets"] = assets + + self.add_child(item_node) + + self.endResetModel() diff --git a/pype/tools/mayalookassigner/views.py b/pype/tools/mayalookassigner/views.py new file mode 100644 index 0000000000..decf04ee57 --- /dev/null +++ b/pype/tools/mayalookassigner/views.py @@ -0,0 +1,50 @@ +from avalon.vendor.Qt import QtWidgets, QtCore + + +DEFAULT_COLOR = "#fb9c15" + + +class View(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + + def __init__(self, parent=None): + super(View, self).__init__(parent=parent) + + # view settings + self.setAlternatingRowColors(False) + self.setSortingEnabled(True) + self.setSelectionMode(self.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + :param indices: The indices to extend. + :type indices: list + + :return: The children indices + :rtype: list + """ + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + else: + # is top level node + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + subitems.add(child) + + return list(subitems) diff --git a/pype/tools/mayalookassigner/widgets.py b/pype/tools/mayalookassigner/widgets.py new file mode 100644 index 0000000000..bfa8492e69 --- /dev/null +++ b/pype/tools/mayalookassigner/widgets.py @@ -0,0 +1,261 @@ +import logging +from collections import defaultdict + +from avalon.vendor.Qt import QtWidgets, QtCore + +# TODO: expose this better in avalon core +from avalon.tools import lib +from avalon.tools.models import TreeModel + +from . import models +from . import commands +from . import views + +from maya import cmds + +MODELINDEX = QtCore.QModelIndex() + + +class AssetOutliner(QtWidgets.QWidget): + + refreshed = QtCore.Signal() + selection_changed = QtCore.Signal() + + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + layout = QtWidgets.QVBoxLayout() + + title = QtWidgets.QLabel("Assets") + title.setAlignment(QtCore.Qt.AlignCenter) + title.setStyleSheet("font-weight: bold; font-size: 12px") + + model = models.AssetModel() + view = views.View() + view.setModel(model) + view.customContextMenuRequested.connect(self.right_mouse_menu) + view.setSortingEnabled(False) + view.setHeaderHidden(True) + view.setIndentation(10) + + from_all_asset_btn = QtWidgets.QPushButton("Get All Assets") + from_selection_btn = QtWidgets.QPushButton("Get Assets From Selection") + + layout.addWidget(title) + layout.addWidget(from_all_asset_btn) + layout.addWidget(from_selection_btn) + layout.addWidget(view) + + # Build connections + from_selection_btn.clicked.connect(self.get_selected_assets) + from_all_asset_btn.clicked.connect(self.get_all_assets) + + selection_model = view.selectionModel() + selection_model.selectionChanged.connect(self.selection_changed) + + self.view = view + self.model = model + + self.setLayout(layout) + + self.log = logging.getLogger(__name__) + + def clear(self): + self.model.clear() + + # fix looks remaining visible when no items present after "refresh" + # todo: figure out why this workaround is needed. + self.selection_changed.emit() + + def add_items(self, items): + """Add new items to the outliner""" + + self.model.add_items(items) + self.refreshed.emit() + + def get_selected_items(self): + """Get current selected items from view + + Returns: + list: list of dictionaries + """ + + selection_model = self.view.selectionModel() + items = [row.data(TreeModel.ItemRole) for row in + selection_model.selectedRows(0)] + + return items + + def get_all_assets(self): + """Add all items from the current scene""" + + with lib.preserve_expanded_rows(self.view): + with lib.preserve_selection(self.view): + self.clear() + nodes = commands.get_all_asset_nodes() + items = commands.create_items_from_nodes(nodes) + self.add_items(items) + + return len(items) > 0 + + def get_selected_assets(self): + """Add all selected items from the current scene""" + + with lib.preserve_expanded_rows(self.view): + with lib.preserve_selection(self.view): + self.clear() + nodes = commands.get_selected_nodes() + items = commands.create_items_from_nodes(nodes) + self.add_items(items) + + def get_nodes(self, selection=False): + """Find the nodes in the current scene per asset.""" + + items = self.get_selected_items() + + # Collect all nodes by hash (optimization) + if not selection: + nodes = cmds.ls(dag=True, long=True) + else: + nodes = commands.get_selected_nodes() + id_nodes = commands.create_asset_id_hash(nodes) + + # Collect the asset item entries per asset + # and collect the namespaces we'd like to apply + assets = dict() + asset_namespaces = defaultdict(set) + for item in items: + asset_id = str(item["asset"]["_id"]) + asset_name = item["asset"]["name"] + asset_namespaces[asset_name].add(item.get("namespace")) + + if asset_name in assets: + continue + + assets[asset_name] = item + assets[asset_name]["nodes"] = id_nodes.get(asset_id, []) + + # Filter nodes to namespace (if only namespaces were selected) + for asset_name in assets: + namespaces = asset_namespaces[asset_name] + + # When None is present there should be no filtering + if None in namespaces: + continue + + # Else only namespaces are selected and *not* the top entry so + # we should filter to only those namespaces. + nodes = assets[asset_name]["nodes"] + nodes = [node for node in nodes if + commands.get_namespace_from_node(node) in namespaces] + assets[asset_name]["nodes"] = nodes + + return assets + + def select_asset_from_items(self): + """Select nodes from listed asset""" + + items = self.get_nodes(selection=False) + nodes = [] + for item in items.values(): + nodes.extend(item["nodes"]) + + commands.select(nodes) + + def right_mouse_menu(self, pos): + """Build RMB menu for asset outliner""" + + active = self.view.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + globalpos = self.view.viewport().mapToGlobal(pos) + + menu = QtWidgets.QMenu(self.view) + + # Direct assignment + apply_action = QtWidgets.QAction(menu, text="Select nodes") + apply_action.triggered.connect(self.select_asset_from_items) + + if not active.isValid(): + apply_action.setEnabled(False) + + menu.addAction(apply_action) + + menu.exec_(globalpos) + + +class LookOutliner(QtWidgets.QWidget): + + menu_apply_action = QtCore.Signal() + + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + # look manager layout + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(10) + + # Looks from database + title = QtWidgets.QLabel("Looks") + title.setAlignment(QtCore.Qt.AlignCenter) + title.setStyleSheet("font-weight: bold; font-size: 12px") + title.setAlignment(QtCore.Qt.AlignCenter) + + model = models.LookModel() + + # Proxy for dynamic sorting + proxy = QtCore.QSortFilterProxyModel() + proxy.setSourceModel(model) + + view = views.View() + view.setModel(proxy) + view.setMinimumHeight(180) + view.setToolTip("Use right mouse button menu for direct actions") + view.customContextMenuRequested.connect(self.right_mouse_menu) + view.sortByColumn(0, QtCore.Qt.AscendingOrder) + + layout.addWidget(title) + layout.addWidget(view) + + self.view = view + self.model = model + + def clear(self): + self.model.clear() + + def add_items(self, items): + self.model.add_items(items) + + def get_selected_items(self): + """Get current selected items from view + + Returns: + list: list of dictionaries + """ + + datas = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()] + items = [d for d in datas if d is not None] # filter Nones + + return items + + def right_mouse_menu(self, pos): + """Build RMB menu for look view""" + + active = self.view.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + globalpos = self.view.viewport().mapToGlobal(pos) + + if not active.isValid(): + return + + menu = QtWidgets.QMenu(self.view) + + # Direct assignment + apply_action = QtWidgets.QAction(menu, text="Assign looks..") + apply_action.triggered.connect(self.menu_apply_action) + + menu.addAction(apply_action) + + menu.exec_(globalpos) + + From 7d00e96aa7e0d36bdf376be78da152ab58483b61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 17:32:28 +0100 Subject: [PATCH 037/100] change import of mayalookassigner in maya implementation --- pype/hosts/maya/api/customize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/api/customize.py b/pype/hosts/maya/api/customize.py index ee3ad4f239..3f3449b3c9 100644 --- a/pype/hosts/maya/api/customize.py +++ b/pype/hosts/maya/api/customize.py @@ -89,7 +89,7 @@ def override_toolbox_ui(): log.warning("Could not import Workfiles tool") try: - import mayalookassigner + from pype.tools import mayalookassigner except Exception: log.warning("Could not import Maya Look assigner tool") From 8b7fe67ffb08e4fdf238b907edd388b4f6f5c4f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 18:01:26 +0100 Subject: [PATCH 038/100] fixed imports --- pype/tools/mayalookassigner/app.py | 6 +++--- pype/tools/mayalookassigner/commands.py | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pype/tools/mayalookassigner/app.py b/pype/tools/mayalookassigner/app.py index eacf373052..92ab7046a8 100644 --- a/pype/tools/mayalookassigner/app.py +++ b/pype/tools/mayalookassigner/app.py @@ -2,7 +2,7 @@ import sys import time import logging -import pype.hosts.maya.lib as cblib +from pype.hosts.maya.api.lib import assign_look_by_version from avalon import style, io from avalon.tools import lib @@ -212,8 +212,8 @@ class App(QtWidgets.QWidget): asset)) # Assign look - cblib.assign_look_by_version(nodes=item["nodes"], - version_id=version["_id"]) + assign_look_by_version(nodes=item["nodes"], + version_id=version["_id"]) end = time.time() diff --git a/pype/tools/mayalookassigner/commands.py b/pype/tools/mayalookassigner/commands.py index d1235bf8c4..a379a109f4 100644 --- a/pype/tools/mayalookassigner/commands.py +++ b/pype/tools/mayalookassigner/commands.py @@ -4,10 +4,7 @@ import os import maya.cmds as cmds -try: - import pype.maya.lib as cblib -except Exception: - import pype.hosts.maya.lib as cblib +from pype.hosts.maya.api import lib from avalon import io, api @@ -105,7 +102,7 @@ def create_asset_id_hash(nodes): """ node_id_hash = defaultdict(list) for node in nodes: - value = cblib.get_id(node) + value = lib.get_id(node) if value is None: continue @@ -149,7 +146,7 @@ def create_items_from_nodes(nodes): continue # Collect available look subsets for this asset - looks = cblib.list_looks(asset["_id"]) + looks = lib.list_looks(asset["_id"]) # Collect namespaces the asset is found in namespaces = set() From 7e2c42d2835fac19f3edec87b51e26664b8c345d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 18:03:59 +0100 Subject: [PATCH 039/100] removed maya-look-assigner submodule --- repos/maya-look-assigner | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/maya-look-assigner diff --git a/repos/maya-look-assigner b/repos/maya-look-assigner deleted file mode 160000 index 7adabe8f0e..0000000000 --- a/repos/maya-look-assigner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7adabe8f0e6858bfe5b6bf0b39bd428ed72d0452 From 8bed81a547f682f8624c7597d670d8ae0100f685 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 18:31:02 +0100 Subject: [PATCH 040/100] remove readme --- pype/tools/mayalookassigner/README.MD | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 pype/tools/mayalookassigner/README.MD diff --git a/pype/tools/mayalookassigner/README.MD b/pype/tools/mayalookassigner/README.MD deleted file mode 100644 index cfd0c15d89..0000000000 --- a/pype/tools/mayalookassigner/README.MD +++ /dev/null @@ -1,10 +0,0 @@ -# Maya Look Assigner - -Tool to assign published lookdev shaders to selected or all objects. -Each shader variation is listed based on the unique asset Id of the -selected or all objects. - -## Dependencies -* Avalon -* Colorbleed configuration for Avalon -* Autodesk Maya 2016 and up From 4f70c7ba94301c66efad7e3f8206f4fea407bb3e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 20:49:23 +0100 Subject: [PATCH 041/100] remove look assigner from submodules --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index a282ef169d..31e7764741 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,9 +5,6 @@ [submodule "repos/avalon-unreal-integration"] path = repos/avalon-unreal-integration url = git@github.com:pypeclub/avalon-unreal-integration.git -[submodule "repos/maya-look-assigner"] - path = repos/maya-look-assigner - url = git@github.com:pypeclub/maya-look-assigner.git [submodule "pype/modules/ftrack/python2_vendor/ftrack-python-api"] path = pype/modules/ftrack/python2_vendor/ftrack-python-api url = https://bitbucket.org/ftrack/ftrack-python-api.git From 5c930f7f3912cfe694b90b21a3c17ded71089ee1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 4 Mar 2021 12:48:06 +0100 Subject: [PATCH 042/100] nuke loaders --- pype/hosts/nuke/plugins/load/load_mov.py | 1 + .../defaults/project_settings/nuke.json | 19 ++++++++++++++ .../projects_schema/schema_project_nuke.json | 5 ++++ .../schemas/schema_nuke_load.json | 26 +++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json diff --git a/pype/hosts/nuke/plugins/load/load_mov.py b/pype/hosts/nuke/plugins/load/load_mov.py index 435f26ad98..830359ccf9 100644 --- a/pype/hosts/nuke/plugins/load/load_mov.py +++ b/pype/hosts/nuke/plugins/load/load_mov.py @@ -95,6 +95,7 @@ class LoadMov(api.Loader): containerise, viewer_update_and_undo_stop ) + version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] diff --git a/pype/settings/defaults/project_settings/nuke.json b/pype/settings/defaults/project_settings/nuke.json index 392f189bd7..5821584932 100644 --- a/pype/settings/defaults/project_settings/nuke.json +++ b/pype/settings/defaults/project_settings/nuke.json @@ -79,6 +79,25 @@ "deadline_chunk_size": 1 } }, + "load": { + "LoadImage": { + "enabled": true, + "representations": [] + }, + "LoadMov": { + "enabled": true, + "representations": [] + }, + "LoadSequence": { + "enabled": true, + "representations": [ + "png", + "jpg", + "exr", + "" + ] + } + }, "workfile_build": { "profiles": [ { diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 0548bd3544..220d56a306 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -45,6 +45,11 @@ "type": "schema", "name": "schema_nuke_publish", "template_data": [] + }, + { + "type": "schema", + "name": "schema_nuke_load", + "template_data": [] }, { "type": "schema", diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json new file mode 100644 index 0000000000..9d132e33b4 --- /dev/null +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json @@ -0,0 +1,26 @@ +{ + "type": "dict", + "collapsible": true, + "key": "load", + "label": "Loader plugins", + "children": [ + { + "type": "schema_template", + "name": "template_loader_plugin", + "template_data": [ + { + "key": "LoadImage", + "label": "Image Loader" + }, + { + "key": "LoadMov", + "label": "Movie Loader" + }, + { + "key": "LoadSequence", + "label": "Image Sequence Loader" + } + ] + } + ] +} From e27e431742419e31d557419cdda4df3286bb4118 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 4 Mar 2021 12:49:23 +0100 Subject: [PATCH 043/100] remove look assigner folder --- repos/maya-look-assigner | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/maya-look-assigner diff --git a/repos/maya-look-assigner b/repos/maya-look-assigner deleted file mode 160000 index 7adabe8f0e..0000000000 --- a/repos/maya-look-assigner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7adabe8f0e6858bfe5b6bf0b39bd428ed72d0452 From 9f1303af07844b6fcc7b0836b6b2862489e9e6d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 18:06:18 +0100 Subject: [PATCH 044/100] fix ordered dict for python 2 hosts --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 56c317ef3d..b4a2760c93 100644 --- a/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/pype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -164,10 +164,9 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # attribute value. # - this is because there may be non hiearchical custom # attributes with different properties - entity_key = collections.OrderedDict({ - "configuration_id": hier_attr["id"], - "entity_id": entity["id"] - }) + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = hier_attr["id"] + entity_key["entity_id"] = entity["id"] self.session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( "ContextCustomAttributeValue", From 7e76320743e54e15c77ec03642e46802caad3185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:03:08 +0100 Subject: [PATCH 045/100] fix frame range of pass output --- .../tvpaint/plugins/publish/collect_instances.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/collect_instances.py b/pype/hosts/tvpaint/plugins/publish/collect_instances.py index 1a5a187c16..efe265e791 100644 --- a/pype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/pype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -70,15 +70,8 @@ class CollectInstances(pyblish.api.ContextPlugin): if instance is None: continue - frame_start = context.data["frameStart"] - frame_end = frame_start - for layer in instance.data["layers"]: - _frame_end = layer["frame_end"] - if _frame_end > frame_end: - frame_end = _frame_end - - instance.data["frameStart"] = frame_start - instance.data["frameEnd"] = frame_end + instance.data["frameStart"] = context.data["frameStart"] + instance.data["frameEnd"] = context.data["frameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) From 573011ae19f5d5262c649c34310dc571895c2828 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:17:12 +0100 Subject: [PATCH 046/100] renamed extract sequence to extract review sequence # Conflicts: # pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py # pype/hosts/tvpaint/plugins/publish/extract_sequence.py # pype/plugins/tvpaint/publish/extract_sequence.py --- ...sequence.py => extract_review_sequence.py} | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) rename pype/hosts/tvpaint/plugins/publish/{extract_sequence.py => extract_review_sequence.py} (89%) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py similarity index 89% rename from pype/hosts/tvpaint/plugins/publish/extract_sequence.py rename to pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py index 17d8dc60f4..54f21cb974 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py @@ -6,44 +6,10 @@ from avalon.tvpaint import lib from PIL import Image -class ExtractSequence(pyblish.api.Extractor): - label = "Extract Sequence" +class ExtractReviewSequence(pyblish.api.Extractor): + label = "Extract Review Sequence" hosts = ["tvpaint"] - families = ["review", "renderPass", "renderLayer"] - - save_mode_to_ext = { - "avi": ".avi", - "bmp": ".bmp", - "cin": ".cin", - "deep": ".dip", - "dps": ".dps", - "dpx": ".dpx", - "flc": ".fli", - "gif": ".gif", - "ilbm": ".iff", - "jpg": ".jpg", - "jpeg": ".jpg", - "pcx": ".pcx", - "png": ".png", - "psd": ".psd", - "qt": ".qt", - "rtv": ".rtv", - "sun": ".ras", - "tiff": ".tiff", - "tga": ".tga", - "vpb": ".vpb" - } - sequential_save_mode = { - "bmp", - "dpx", - "ilbm", - "jpg", - "jpeg", - "png", - "sun", - "tiff", - "tga" - } + families = ["review"] def process(self, instance): self.log.info( From 901287ab797634c60c10392f5218180f75ef1d6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:18:40 +0100 Subject: [PATCH 047/100] moved back tvpaint's lib for compositing --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 0000000000..8172392c7f --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) From ddd23d1b14b7f1e888c6377ae6a78ed58191d4d6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:19:57 +0100 Subject: [PATCH 048/100] implemented extract review that can render layer by layer with alpha # Conflicts: # pype/hosts/tvpaint/plugins/publish/extract_sequence.py --- .../plugins/publish/extract_sequence.py | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 pype/hosts/tvpaint/plugins/publish/extract_sequence.py diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py new file mode 100644 index 0000000000..035f50c058 --- /dev/null +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -0,0 +1,492 @@ +import os +import shutil +import time +import tempfile +import multiprocessing + +import pyblish.api +from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images +from PIL import Image + + +class ExtractSequence(pyblish.api.Extractor): + label = "Extract Sequence" + hosts = ["tvpaint"] + families = ["renderPass", "renderLayer"] + + def process(self, instance): + self.log.info( + "* Processing instance \"{}\"".format(instance.data["label"]) + ) + + # Get all layers and filter out not visible + layers = instance.data["layers"] + filtered_layers = [ + layer + for layer in layers + if layer["visible"] + ] + layer_names = [str(layer["name"]) for layer in filtered_layers] + if not layer_names: + self.log.info( + "None of the layers from the instance" + " are visible. Extraction skipped." + ) + return + + joined_layer_names = ", ".join( + ["\"{}\"".format(name) for name in layer_names] + ) + self.log.debug( + "Instance has {} layers with names: {}".format( + len(layer_names), joined_layer_names + ) + ) + + family_lowered = instance.data["family"].lower() + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] + + filename_template = self._get_filename_template(frame_end) + ext = os.path.splitext(filename_template)[1].replace(".", "") + + self.log.debug("Using file template \"{}\"".format(filename_template)) + + # Save to staging dir + output_dir = instance.data.get("stagingDir") + if not output_dir: + # Create temp folder if staging dir is not set + output_dir = tempfile.mkdtemp().replace("\\", "/") + instance.data["stagingDir"] = output_dir + + self.log.debug( + "Files will be rendered to folder: {}".format(output_dir) + ) + + thumbnail_filename = "thumbnail.jpg" + + # Render output + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ) + + # Fill tags and new families + tags = [] + if family_lowered in ("review", "renderlayer"): + tags.append("review") + + repre_files = [ + os.path.basename(filepath) + for filepath in output_filepaths + ] + # Sequence of one frame + if len(repre_files) == 1: + repre_files = repre_files[0] + + new_repre = { + "name": ext, + "ext": ext, + "files": repre_files, + "stagingDir": output_dir, + "frameStart": frame_start, + "frameEnd": frame_end, + "tags": tags + } + self.log.debug("Creating new representation: {}".format(new_repre)) + + instance.data["representations"].append(new_repre) + + if family_lowered in ("renderpass", "renderlayer"): + # Change family to render + instance.data["family"] = "render" + + if not thumbnail_fullpath: + return + + thumbnail_ext = os.path.splitext( + thumbnail_fullpath + )[1].replace(".", "") + # Create thumbnail representation + thumbnail_repre = { + "name": "thumbnail", + "ext": thumbnail_ext, + "outputName": "thumb", + "files": os.path.basename(thumbnail_fullpath), + "stagingDir": output_dir, + "tags": ["thumbnail"] + } + instance.data["representations"].append(thumbnail_repre) + + def _get_filename_template(self, frame_end): + """Get filetemplate for rendered files. + + This is simple template contains `{frame}{ext}` for sequential outputs + and `single_file{ext}` for single file output. Output is rendered to + temporary folder so filename should not matter as integrator change + them. + """ + frame_padding = 4 + frame_end_str_len = len(str(frame_end)) + if frame_end_str_len > frame_padding: + frame_padding = frame_end_str_len + + return "{{:0>{}}}".format(frame_padding) + ".png" + + def render( + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ): + """ Export images from TVPaint. + + Args: + save_mode (str): Argument for `tv_savemode` george script function. + More about save mode in documentation. + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + layers (list): List of layers to be exported. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + + # Map layers by position + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) + + # Sort layer positions in reverse order + sorted_positions = list(reversed(sorted(layers_by_position.keys()))) + if not sorted_positions: + return + + self.log.debug("Collecting pre/post behavior of individual layers.") + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} + for position in sorted_positions: + layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) + return output + + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename, scene_width, scene_height + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) + ) + + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From 7452ac7da520f02408c0703cdd1888a2c81f6d6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:27:35 +0100 Subject: [PATCH 049/100] modified extract sequence to skip compositing is rendering only one layer --- .../plugins/publish/extract_sequence.py | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 035f50c058..ad87ebbd81 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -183,31 +183,55 @@ class ExtractSequence(pyblish.api.Extractor): tmp_filename_template = "pos_{}." + filename_template files_by_position = {} + is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] + + if is_single_layer: + _template = filename_template + else: + _template = tmp_filename_template + files_by_frames = self.render_layer( layer, - tmp_filename_template, + _template, output_dir, behavior, mark_in_index, mark_out_index ) - files_by_position[position] = files_by_frames + if is_single_layer: + output_filepaths = list(files_by_frames.values()) + else: + files_by_position[position] = files_by_frames - output = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) - return output + if not is_single_layer: + output_filepaths = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) + + thumbnail_src_filepath = None + thumbnail_filepath = None + if output_filepaths: + thumbnail_src_filepath = tuple(sorted(output_filepaths))[0] + + if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath def render_layer( self, @@ -469,15 +493,7 @@ class ExtractSequence(pyblish.api.Extractor): time.sleep(0.01) - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath + return output_filepaths def _cleanup_tmp_files(self, files_by_position): for data in files_by_position.values(): From 4ea35c2ce810bc2f5f20eabeebfd6947118e8a03 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:58:29 +0100 Subject: [PATCH 050/100] removed thumbnail filename variable --- .../tvpaint/plugins/publish/extract_sequence.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index ad87ebbd81..a66141fa19 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -66,13 +66,10 @@ class ExtractSequence(pyblish.api.Extractor): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" - # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ) # Fill tags and new families @@ -139,8 +136,7 @@ class ExtractSequence(pyblish.api.Extractor): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ): """ Export images from TVPaint. @@ -213,7 +209,6 @@ class ExtractSequence(pyblish.api.Extractor): mark_in_index, mark_out_index, filename_template, - thumbnail_filename, scene_width, scene_height ) @@ -226,7 +221,7 @@ class ExtractSequence(pyblish.api.Extractor): if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) @@ -434,7 +429,7 @@ class ExtractSequence(pyblish.api.Extractor): def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height + filename_template, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -452,7 +447,6 @@ class ExtractSequence(pyblish.api.Extractor): processes = {} output_filepaths = [] - thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 @@ -461,9 +455,6 @@ class ExtractSequence(pyblish.api.Extractor): output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - processes[frame_idx] = multiprocessing.Process( target=composite_images, args=( From da12476f684e926237f984b38fa5dd1f8cf65ce9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:21:06 +0100 Subject: [PATCH 051/100] merged extractors to one extractor # Conflicts: # pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py --- .../publish/extract_review_sequence.py | 206 ------------------ .../plugins/publish/extract_sequence.py | 88 ++++++-- 2 files changed, 74 insertions(+), 220 deletions(-) delete mode 100644 pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py diff --git a/pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py deleted file mode 100644 index 54f21cb974..0000000000 --- a/pype/hosts/tvpaint/plugins/publish/extract_review_sequence.py +++ /dev/null @@ -1,206 +0,0 @@ -import os -import tempfile - -import pyblish.api -from avalon.tvpaint import lib -from PIL import Image - - -class ExtractReviewSequence(pyblish.api.Extractor): - label = "Extract Review Sequence" - hosts = ["tvpaint"] - families = ["review"] - - def process(self, instance): - self.log.info( - "* Processing instance \"{}\"".format(instance.data["label"]) - ) - - # Get all layers and filter out not visible - layers = instance.data["layers"] - filtered_layers = [ - layer - for layer in layers - if layer["visible"] - ] - filtered_layer_ids = [ - layer["layer_id"] - for layer in filtered_layers - ] - layer_names = [str(layer["name"]) for layer in filtered_layers] - if not layer_names: - self.log.info( - "None of the layers from the instance" - " are visible. Extraction skipped." - ) - return - - joined_layer_names = ", ".join( - ["\"{}\"".format(name) for name in layer_names] - ) - self.log.debug( - "Instance has {} layers with names: {}".format( - len(layer_names), joined_layer_names - ) - ) - - family_lowered = instance.data["family"].lower() - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - filename_template = self._get_filename_template(frame_end) - ext = os.path.splitext(filename_template)[1].replace(".", "") - - self.log.debug("Using file template \"{}\"".format(filename_template)) - - # Save to staging dir - output_dir = instance.data.get("stagingDir") - if not output_dir: - # Create temp folder if staging dir is not set - output_dir = tempfile.mkdtemp().replace("\\", "/") - instance.data["stagingDir"] = output_dir - - self.log.debug( - "Files will be rendered to folder: {}".format(output_dir) - ) - - first_frame_filename = filename_template.format(frame_start) - first_frame_filepath = os.path.join(output_dir, first_frame_filename) - - # Store layers visibility - layer_visibility_by_id = {} - for layer in instance.context.data["layersData"]: - layer_id = layer["layer_id"] - layer_visibility_by_id[layer_id] = layer["visible"] - - george_script_lines = [] - for layer_id in layer_visibility_by_id.keys(): - visible = layer_id in filtered_layer_ids - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Render output - repre_files = self.render( - filename_template, - output_dir, - frame_start, - frame_end - ) - - # Restore visibility - george_script_lines = [] - for layer_id, visible in layer_visibility_by_id.items(): - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - if os.path.exists(first_frame_filepath): - source_img = Image.open(first_frame_filepath) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - # Fill tags and new families - tags = [] - if family_lowered in ("review", "renderlayer"): - tags.append("review") - - # Sequence of one frame - if len(repre_files) == 1: - repre_files = repre_files[0] - - new_repre = { - "name": ext, - "ext": ext, - "files": repre_files, - "stagingDir": output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "tags": tags - } - self.log.debug("Creating new representation: {}".format(new_repre)) - - instance.data["representations"].append(new_repre) - - if family_lowered in ("renderpass", "renderlayer"): - # Change family to render - instance.data["family"] = "render" - - if not os.path.exists(thumbnail_filepath): - return - - thumbnail_ext = os.path.splitext( - thumbnail_filepath - )[1].replace(".", "") - # Create thumbnail representation - thumbnail_repre = { - "name": "thumbnail", - "ext": thumbnail_ext, - "outputName": "thumb", - "files": os.path.basename(thumbnail_filepath), - "stagingDir": output_dir, - "tags": ["thumbnail"] - } - instance.data["representations"].append(thumbnail_repre) - - def _get_filename_template(self, frame_end): - """Get filetemplate for rendered files. - - This is simple template contains `{frame}{ext}` for sequential outputs - and `single_file{ext}` for single file output. Output is rendered to - temporary folder so filename should not matter as integrator change - them. - """ - frame_padding = 4 - frame_end_str_len = len(str(frame_end)) - if frame_end_str_len > frame_padding: - frame_padding = frame_end_str_len - - return "{{:0>{}}}".format(frame_padding) + ".png" - - def render(self, filename_template, output_dir, frame_start, frame_end): - """ Export images from TVPaint. - - Args: - filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. - - Retruns: - dict: Mapping frame to output filepath. - """ - self.log.debug("Preparing data for rendering.") - first_frame_filepath = os.path.join( - output_dir, - filename_template.format(frame_start, frame=frame_start) - ) - mark_in = frame_start - 1 - mark_out = frame_end - 1 - - george_script_lines = [ - "tv_SaveMode \"PNG\"", - "export_path = \"{}\"".format( - first_frame_filepath.replace("\\", "/") - ), - "tv_savesequence '\"'export_path'\"' {} {}".format( - mark_in, mark_out - ) - ] - lib.execute_george_through_file("\n".join(george_script_lines)) - - output = [] - for frame in range(frame_start, frame_end + 1): - output.append( - filename_template.format(frame, frame=frame) - ) - return output diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index a66141fa19..6fe35f6251 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -13,7 +13,7 @@ from PIL import Image class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] - families = ["renderPass", "renderLayer"] + families = ["review", "renderPass", "renderLayer"] def process(self, instance): self.log.info( @@ -66,21 +66,22 @@ class ExtractSequence(pyblish.api.Extractor): "Files will be rendered to folder: {}".format(output_dir) ) - # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height - ) + if instance.data["family"] == "review": + repre_files, thumbnail_fullpath = self.render_review( + filename_template, output_dir, frame_start, frame_end + ) + else: + # Render output + repre_files, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, scene_width, scene_height + ) # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -134,6 +135,58 @@ class ExtractSequence(pyblish.api.Extractor): return "{{:0>{}}}".format(frame_padding) + ".png" + def render_review( + self, filename_template, output_dir, frame_start, frame_end + ): + """ Export images from TVPaint. + + Args: + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + first_frame_filepath = os.path.join( + output_dir, + filename_template.format(frame_start, frame=frame_start) + ) + mark_in = frame_start - 1 + mark_out = frame_end - 1 + + george_script_lines = [ + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) + ] + lib.execute_george_through_file("\n".join(george_script_lines)) + + output = [] + first_frame_filepath = None + for frame in range(frame_start, frame_end + 1): + filename = filename_template.format(frame, frame=frame) + output.append(filename) + if first_frame_filepath is None: + first_frame_filepath = os.path.join(output_dir, filename) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if first_frame_filepath and os.path.exists(first_frame_filepath): + source_img = Image.open(first_frame_filepath) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + return output, thumbnail_filepath + def render( self, filename_template, output_dir, layers, frame_start, frame_end, scene_width, scene_height @@ -189,7 +242,7 @@ class ExtractSequence(pyblish.api.Extractor): else: _template = tmp_filename_template - files_by_frames = self.render_layer( + files_by_frames = self._render_layer( layer, _template, output_dir, @@ -226,9 +279,13 @@ class ExtractSequence(pyblish.api.Extractor): thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return output_filepaths, thumbnail_filepath + repre_files = [ + os.path.basename(path) + for path in output_filepaths + ] + return repre_files, thumbnail_filepath - def render_layer( + def _render_layer( self, layer, tmp_filename_template, @@ -282,7 +339,10 @@ class ExtractSequence(pyblish.api.Extractor): if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath From e6e4954f791b0430c890498c88202907a1563ed7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:31:53 +0100 Subject: [PATCH 052/100] extract sequence use key word arguments in filename template --- .../plugins/publish/extract_sequence.py | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 6fe35f6251..b7f01982ed 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -133,7 +133,7 @@ class ExtractSequence(pyblish.api.Extractor): if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{:0>{}}}".format(frame_padding) + ".png" + return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( self, filename_template, output_dir, frame_start, frame_end @@ -142,9 +142,9 @@ class ExtractSequence(pyblish.api.Extractor): Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. + should already contain extension. Template must contain + keyword argument `{frame}`. Extension in template must match + `save_mode`. output_dir (list): List of layers to be exported. frame_start (int): Starting frame from which export will begin. frame_end (int): On which frame export will end. @@ -155,7 +155,7 @@ class ExtractSequence(pyblish.api.Extractor): self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( output_dir, - filename_template.format(frame_start, frame=frame_start) + filename_template.format(frame=frame_start) ) mark_in = frame_start - 1 mark_out = frame_end - 1 @@ -174,7 +174,7 @@ class ExtractSequence(pyblish.api.Extractor): output = [] first_frame_filepath = None for frame in range(frame_start, frame_end + 1): - filename = filename_template.format(frame, frame=frame) + filename = filename_template.format(frame=frame) output.append(filename) if first_frame_filepath is None: first_frame_filepath = os.path.join(output_dir, filename) @@ -229,7 +229,7 @@ class ExtractSequence(pyblish.api.Extractor): mark_in_index = frame_start - 1 mark_out_index = frame_end - 1 - tmp_filename_template = "pos_{}." + filename_template + tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} is_single_layer = len(sorted_positions) == 1 @@ -310,7 +310,10 @@ class ExtractSequence(pyblish.api.Extractor): layer_position = layer["position"] for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) dst_path = "/".join([output_dir, filename]) layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) @@ -397,7 +400,10 @@ class ExtractSequence(pyblish.api.Extractor): # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -411,7 +417,10 @@ class ExtractSequence(pyblish.api.Extractor): eq_frame_idx = frame_end_index - eq_frame_idx_offset eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -427,7 +436,10 @@ class ExtractSequence(pyblish.api.Extractor): eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -455,7 +467,10 @@ class ExtractSequence(pyblish.api.Extractor): # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -466,7 +481,10 @@ class ExtractSequence(pyblish.api.Extractor): eq_frame_idx = frame_idx % frame_count eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -482,7 +500,10 @@ class ExtractSequence(pyblish.api.Extractor): eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -511,7 +532,7 @@ class ExtractSequence(pyblish.api.Extractor): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 - output_filename = filename_template.format(frame) + output_filename = filename_template.format(frame=frame) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) From a34c14f0f1e4d593fef081418eae5870e5a03eaa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:46:58 +0100 Subject: [PATCH 053/100] handle "none" behavior --- .../plugins/publish/extract_sequence.py | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index b7f01982ed..2c318136e6 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -7,7 +7,7 @@ import multiprocessing import pyblish.api from avalon.tvpaint import lib from pype.hosts.tvpaint.lib import composite_images -from PIL import Image +from PIL import Image, ImageDraw class ExtractSequence(pyblish.api.Extractor): @@ -394,9 +394,29 @@ class ExtractSequence(pyblish.api.Extractor): return if pre_behavior == "none": - return + # Take size from first image and fill it with transparent color + first_filename = filename_template.format( + pos=layer_position, + frame=frame_start_index + ) + first_filepath = os.path.join(output_dir, first_filename) + empty_image_filepath = None + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(first_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) - if pre_behavior == "hold": + elif pre_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): @@ -461,9 +481,29 @@ class ExtractSequence(pyblish.api.Extractor): return if post_behavior == "none": - return + # Take size from last image and fill it with transparent color + last_filename = filename_template.format( + pos=layer_position, + frame=frame_end_index + ) + last_filepath = os.path.join(output_dir, last_filename) + empty_image_filepath = None + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(last_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) - if post_behavior == "hold": + elif post_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): From 9dc1e38341ef7229ad3b013c684bdc332a8a1f27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:41 +0100 Subject: [PATCH 054/100] add files to `layer_files_by_frame` on creation --- .../plugins/publish/extract_sequence.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 2c318136e6..667799a7cd 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -406,15 +406,16 @@ class ExtractSequence(pyblish.api.Extractor): pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(first_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif pre_behavior == "hold": # Keep first frame for whole time @@ -493,15 +494,16 @@ class ExtractSequence(pyblish.api.Extractor): pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(last_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif post_behavior == "hold": # Keep first frame for whole time From b5aa524747241466c04608fde51ad201ef6879a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:52 +0100 Subject: [PATCH 055/100] added some debug loggins messages --- .../plugins/publish/extract_sequence.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 667799a7cd..8f302cb746 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -300,6 +300,9 @@ class ExtractSequence(pyblish.api.Extractor): exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer["layer_id"], layer["name"] + )) if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -333,6 +336,7 @@ class ExtractSequence(pyblish.api.Extractor): "Filling frames between first and last frame of layer ({} - {})." ).format(frame_start_index + 1, frame_end_index + 1)) + _debug_filled_frames = [] prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -341,7 +345,7 @@ class ExtractSequence(pyblish.api.Extractor): if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - + _debug_filled_frames.append(frame_idx) filename = tmp_filename_template.format( pos=layer_position, frame=frame_idx @@ -350,6 +354,8 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + self.log.debug("Filled frames {}".format(str(_debug_filled_frames))) + # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] @@ -391,9 +397,16 @@ class ExtractSequence(pyblish.api.Extractor): frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_in_index >= frame_start_index: + self.log.debug(( + "Skipping pre-behavior." + " All frames after Mark In are rendered." + )) return if pre_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + mark_in_index, frame_start_index + )) # Take size from first image and fill it with transparent color first_filename = filename_template.format( pos=layer_position, @@ -479,9 +492,16 @@ class ExtractSequence(pyblish.api.Extractor): frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_out_index <= frame_end_index: + self.log.debug(( + "Skipping post-behavior." + " All frames up to Mark Out are rendered." + )) return if post_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + frame_end_index + 1, mark_out_index + 1 + )) # Take size from last image and fill it with transparent color last_filename = filename_template.format( pos=layer_position, From e79c8c6a6d2bb0eb43f91989eb3226e5398e4a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:37 +0100 Subject: [PATCH 056/100] review instance stores copy of layers data --- pype/hosts/tvpaint/plugins/publish/collect_instances.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/hosts/tvpaint/plugins/publish/collect_instances.py b/pype/hosts/tvpaint/plugins/publish/collect_instances.py index efe265e791..57602d9610 100644 --- a/pype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/pype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -48,7 +48,10 @@ class CollectInstances(pyblish.api.ContextPlugin): instance_data["subset"] = new_subset_name instance = context.create_instance(**instance_data) - instance.data["layers"] = context.data["layersData"] + + instance.data["layers"] = copy.deepcopy( + context.data["layersData"] + ) # Add ftrack family instance.data["families"].append("ftrack") From 3360f996c4a7c2cf71ffb595754288ccf3db3172 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:53 +0100 Subject: [PATCH 057/100] added validation of layers visibility # Conflicts: # pype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py --- .../publish/validate_layers_visibility.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py diff --git a/pype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/pype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py new file mode 100644 index 0000000000..74ef34169e --- /dev/null +++ b/pype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -0,0 +1,16 @@ +import pyblish.api + + +class ValidateLayersVisiblity(pyblish.api.InstancePlugin): + """Validate existence of renderPass layers.""" + + label = "Validate Layers Visibility" + order = pyblish.api.ValidatorOrder + families = ["review", "renderPass", "renderLayer"] + + def process(self, instance): + for layer in instance.data["layers"]: + if layer["visible"]: + return + + raise AssertionError("All layers of instance are not visible.") From 3fc3355060d671cd117a8f072f108de797226c4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 18:21:58 +0100 Subject: [PATCH 058/100] modified to match pype 3 structure --- pype/hosts/tvpaint/{ => api}/lib.py | 0 pype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pype/hosts/tvpaint/{ => api}/lib.py (100%) diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/api/lib.py similarity index 100% rename from pype/hosts/tvpaint/lib.py rename to pype/hosts/tvpaint/api/lib.py diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8f302cb746..14a74885ee 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -6,7 +6,7 @@ import multiprocessing import pyblish.api from avalon.tvpaint import lib -from pype.hosts.tvpaint.lib import composite_images +from pype.hosts.tvpaint.api.lib import composite_images from PIL import Image, ImageDraw From 673b55851bc4627deb5d244ff336da8b721c6734 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 20:43:29 +0100 Subject: [PATCH 059/100] extraction is a little bit faster # Conflicts: # pype/plugins/tvpaint/publish/extract_sequence.py --- pype/hosts/tvpaint/api/lib.py | 15 +- .../plugins/publish/extract_sequence.py | 205 ++++++++---------- 2 files changed, 105 insertions(+), 115 deletions(-) diff --git a/pype/hosts/tvpaint/api/lib.py b/pype/hosts/tvpaint/api/lib.py index 8172392c7f..4267129fe6 100644 --- a/pype/hosts/tvpaint/api/lib.py +++ b/pype/hosts/tvpaint/api/lib.py @@ -1,9 +1,15 @@ from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): +def composite_images(input_image_paths, output_filepath): + """Composite images in order from passed list. + + Raises: + ValueError: When entered list is empty. + """ + if not input_image_paths: + raise ValueError("Nothing to composite.") + img_obj = None for image_filepath in input_image_paths: _img_obj = Image.open(image_filepath) @@ -11,7 +17,4 @@ def composite_images( img_obj = _img_obj else: img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) img_obj.save(output_filepath) diff --git a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py index 14a74885ee..41117851b7 100644 --- a/pype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/pype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -47,8 +47,6 @@ class ExtractSequence(pyblish.api.Extractor): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -73,8 +71,8 @@ class ExtractSequence(pyblish.api.Extractor): else: # Render output repre_files, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height + filename_template, output_dir, frame_start, frame_end, + filtered_layers ) # Fill tags and new families @@ -138,19 +136,20 @@ class ExtractSequence(pyblish.api.Extractor): def render_review( self, filename_template, output_dir, frame_start, frame_end ): - """ Export images from TVPaint. + """ Export images from TVPaint using `tv_savesequence` command. Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template must contain - keyword argument `{frame}`. Extension in template must match - `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (str): Directory where files will be stored. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( @@ -188,24 +187,23 @@ class ExtractSequence(pyblish.api.Extractor): return output, thumbnail_filepath def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, scene_width, scene_height + self, filename_template, output_dir, frame_start, frame_end, layers ): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. + output_dir (str): Directory where files will be stored. first_frame (int): Starting frame from which export will begin. last_frame (int): On which frame export will end. + layers (list): List of layers to be exported. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") @@ -232,40 +230,28 @@ class ExtractSequence(pyblish.api.Extractor): tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} - is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] - if is_single_layer: - _template = filename_template - else: - _template = tmp_filename_template - files_by_frames = self._render_layer( layer, - _template, + tmp_filename_template, output_dir, behavior, mark_in_index, mark_out_index ) - if is_single_layer: - output_filepaths = list(files_by_frames.values()) - else: - files_by_position[position] = files_by_frames + files_by_position[position] = files_by_frames - if not is_single_layer: - output_filepaths = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) + output_filepaths = self._composite_files( + files_by_position, + mark_in_index, + mark_out_index, + filename_template, + output_dir + ) + self._cleanup_tmp_files(files_by_position) thumbnail_src_filepath = None thumbnail_filepath = None @@ -300,9 +286,7 @@ class ExtractSequence(pyblish.api.Extractor): exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) - self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( - str(exposure_frames), layer["layer_id"], layer["name"] - )) + if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -325,8 +309,8 @@ class ExtractSequence(pyblish.api.Extractor): # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer_id, layer["name"] )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) @@ -404,31 +388,8 @@ class ExtractSequence(pyblish.api.Extractor): return if pre_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - mark_in_index, frame_start_index - )) - # Take size from first image and fill it with transparent color - first_filename = filename_template.format( - pos=layer_position, - frame=frame_start_index - ) - first_filepath = os.path.join(output_dir, first_filename) - empty_image_filepath = None - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(first_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif pre_behavior == "hold": # Keep first frame for whole time @@ -499,31 +460,8 @@ class ExtractSequence(pyblish.api.Extractor): return if post_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - frame_end_index + 1, mark_out_index + 1 - )) - # Take size from last image and fill it with transparent color - last_filename = filename_template.format( - pos=layer_position, - frame=frame_end_index - ) - last_filepath = os.path.join(output_dir, last_filename) - empty_image_filepath = None - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(last_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif post_behavior == "hold": # Keep first frame for whole time @@ -571,9 +509,16 @@ class ExtractSequence(pyblish.api.Extractor): layer_files_by_frame[frame_idx] = new_filepath def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, scene_width, scene_height + self, files_by_position, frame_start, frame_end, + filename_template, output_dir ): + """Composite frames when more that one layer was exported. + + This method is used when more than one layer is rendered out so and + output should be composition of each frame of rendered layers. + Missing frames are filled with transparent images. + """ + self.log.debug("Preparing files for compisiting.") # Prepare paths to images by frames into list where are stored # in order of compositing. images_by_frame = {} @@ -582,7 +527,8 @@ class ExtractSequence(pyblish.api.Extractor): for position in sorted(files_by_position.keys(), reverse=True): position_data = files_by_position[position] if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) + filepath = position_data[frame_idx] + images_by_frame[frame_idx].append(filepath) process_count = os.cpu_count() if process_count > 1: @@ -590,22 +536,41 @@ class ExtractSequence(pyblish.api.Extractor): processes = {} output_filepaths = [] + missing_frame_paths = [] + random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame=frame) + output_filename = filename_template.format(frame=frame_idx + 1) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height - ) - ) + # Store information about missing frame and skip + if not image_filepaths: + missing_frame_paths.append(output_filepath) + continue - # Wait until all processes are done + # Just rename the file if is no need of compositing + if len(image_filepaths) == 1: + os.rename(image_filepaths[0], output_filepath) + + # Prepare process for compositing of images + else: + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=(image_filepaths, output_filepath) + ) + + # Store path of random output image that will 100% exist after all + # multiprocessing as mockup for missing frames + if random_frame_path is None: + random_frame_path = output_filepath + + self.log.info( + "Running {} compositing processes - this mey take a while.".format( + len(processes) + ) + ) + # Wait until all compositing processes are done running_processes = {} while True: for idx in tuple(running_processes.keys()): @@ -627,14 +592,36 @@ class ExtractSequence(pyblish.api.Extractor): time.sleep(0.01) + self.log.debug( + "Creating transparent images for frames without render {}.".format( + str(missing_frame_paths) + ) + ) + # Fill the sequence with transparent frames + transparent_filepath = None + for filepath in missing_frame_paths: + if transparent_filepath is None: + img_obj = Image.open(random_frame_path) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + transparent_filepath = filepath + else: + self._copy_image(transparent_filepath, filepath) return output_filepaths def _cleanup_tmp_files(self, files_by_position): + """Remove temporary files that were used for compositing.""" for data in files_by_position.values(): for filepath in data.values(): - os.remove(filepath) + if os.path.exists(filepath): + os.remove(filepath) def _copy_image(self, src_path, dst_path): + """Create a copy of an image. + + This was added to be able easier change copy method. + """ # Create hardlink of image instead of copying if possible if hasattr(os, "link"): os.link(src_path, dst_path) From ceea42e506acd35c35d882198d055426224e592a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 10:15:24 +0100 Subject: [PATCH 060/100] fix versions in standalone publisher --- pype/tools/standalonepublish/widgets/widget_family.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 5521503508..3150646624 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -340,11 +340,8 @@ class FamilyWidget(QtWidgets.QWidget): ).distinct("name") if versions: - versions = sorted( - [v for v in versions], - key=lambda ver: ver['name'] - ) - version = int(versions[-1]['name']) + 1 + versions = sorted(versions) + version = int(versions[-1]) + 1 self.version_spinbox.setValue(version) From 80d7bb8067e037ee180598ac0b23028504dd3c9e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Mar 2021 19:01:44 +0100 Subject: [PATCH 061/100] PS - added Subset Manager into menu Implemented list_instances and remove_instance methods --- .../websocket_server/hosts/photoshop.py | 3 +++ .../stubs/photoshop_server_stub.py | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index cdfb9413a0..b75874aa6c 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -54,6 +54,9 @@ class Photoshop(WebSocketRoute): async def projectmanager_route(self): self._tool_route("projectmanager") + async def subsetmanager_route(self): + self._tool_route("subsetmanager") + def _tool_route(self, tool_name): """The address accessed when clicking on the buttons.""" partial_method = functools.partial(photoshop.show, tool_name) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index d223153797..9677fa61a8 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -238,7 +238,14 @@ class PhotoshopServerStub(): """ Reads layers metadata from Headline from active document in PS. (Headline accessible by File > File Info) - Returns(string): - json documents + + Returns: + (string): - json documents + example: + {"8":{"active":true,"subset":"imageBG", + "family":"image","id":"pyblish.avalon.instance", + "asset":"Town"}} + 8 is layer(group) id - used for deletion, update etc. """ layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) @@ -288,6 +295,19 @@ class PhotoshopServerStub(): ('Photoshop.delete_layer', layer_id=layer_id)) + def remove_instance(self, instance_id): + cleaned_data = {} + + for key, instance in self.get_layers_metadata().items(): + if key != instance_id: + cleaned_data[key] = instance + + payload = json.dumps(cleaned_data, indent=4) + + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) + def close(self): self.client.close() From e19b960096e0a0b7879346678cdde883b771a70f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:02:23 +0100 Subject: [PATCH 062/100] PS - use subset name as instance name Warn if duplicate subsets found --- .../hosts/photoshop/plugins/publish/collect_instances.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/hosts/photoshop/plugins/publish/collect_instances.py b/pype/hosts/photoshop/plugins/publish/collect_instances.py index 14803cceee..5390df768b 100644 --- a/pype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/pype/hosts/photoshop/plugins/publish/collect_instances.py @@ -24,6 +24,7 @@ class CollectInstances(pyblish.api.ContextPlugin): stub = photoshop.stub() layers = stub.get_layers() layers_meta = stub.get_layers_metadata() + instance_names = [] for layer in layers: layer_data = stub.read(layer, layers_meta) @@ -41,14 +42,20 @@ class CollectInstances(pyblish.api.ContextPlugin): # self.log.info("%s skipped, it was empty." % layer.Name) # continue - instance = context.create_instance(layer.name) + instance = context.create_instance(layer_data["subset"]) instance.append(layer) instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ layer_data["family"] ] instance.data["publish"] = layer.visible + instance_names.append(layer_data["subset"]) # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.info("instance: {} ".format(instance.data)) + + if len(instance_names) != len(set(instance_names)): + self.log.warning("Duplicate instances found. " + + "Remove unwanted via SubsetManager") From 514dbfc2af08d2c2635bf3c1632021726312031a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:54:29 +0100 Subject: [PATCH 063/100] PS - added validator for unique subsets --- .../publish/validate_unique_subsets.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py diff --git a/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py new file mode 100644 index 0000000000..5871f79668 --- /dev/null +++ b/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -0,0 +1,26 @@ +import pyblish.api +import pype.api + + +class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): + """ + Validate that all subset's names are unique. + """ + + label = "Validate Subset Uniqueness" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + + def process(self, context): + subset_names = [] + + for instance in context: + if instance.data.get('publish'): + subset_names.append(instance.data.get('subset')) + + msg = ( + "Instance subset names are not unique. " + + "Remove duplicates via SubsetManager." + ) + assert len(subset_names) == len(set(subset_names)), msg From e414ac8a2db4bb094b938d12d6877312bae5a025 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 16:24:24 +0100 Subject: [PATCH 064/100] PS - fixed validate_naming repair --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 2483adcb5e..02f6fdcacb 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -30,6 +30,10 @@ class ValidateNamingRepair(pyblish.api.Action): data["subset"] = "image" + name stub.imprint(instance[0], data) + name = name.replace(instance.data["family"], '') + name = stub.PUBLISH_ICON + name + stub.rename_layer(instance.data["uuid"], name) + return True From ec06cc9c4625b3666fa617d516d69dbfa4107257 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 11:41:34 +0100 Subject: [PATCH 065/100] PS - added highlight with icon for publishable instances --- pype/hosts/photoshop/plugins/create/create_image.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/hosts/photoshop/plugins/create/create_image.py b/pype/hosts/photoshop/plugins/create/create_image.py index 54b6efad29..03250acd48 100644 --- a/pype/hosts/photoshop/plugins/create/create_image.py +++ b/pype/hosts/photoshop/plugins/create/create_image.py @@ -73,5 +73,17 @@ class CreateImage(pype.api.Creator): groups.append(group) for group in groups: + long_names = [] + if group.long_name: + for directory in group.long_name[::-1]: + name = directory.replace(stub.PUBLISH_ICON, '').\ + replace(stub.LOADED_ICON, '') + long_names.append(name) + self.data.update({"subset": "image" + group.name}) + self.data.update({"uuid": str(group.id)}) + self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) + # reusing existing group, need to rename afterwards + if not create_group: + stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) From 065266cf40bb541b05a8e0caa71baa6f2bcc45a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:03:58 +0100 Subject: [PATCH 066/100] PS - added highlight with icon for publishable instances Changed structure of metadata from {} to [] Added rename_layer method Switched to attr instead of namedtuple (same as in AE) --- .../stubs/photoshop_server_stub.py | 163 +++++++++++++++--- 1 file changed, 135 insertions(+), 28 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 9677fa61a8..79a486a20c 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -4,16 +4,37 @@ from pype.modules.websocket_server import WebSocketServer Used anywhere solution is calling client methods. """ import json -from collections import namedtuple +import attr -class PhotoshopServerStub(): +@attr.s +class PSItem(object): + """ + Object denoting layer or group item in PS. Each item is created in + PS by any Loader, but contains same fields, which are being used + in later processing. + """ + # metadata + id = attr.ib() # id created by AE, could be used for querying + name = attr.ib() # name of item + group = attr.ib(default=None) # item type (footage, folder, comp) + parents = attr.ib(factory=list) + visible = attr.ib(default=True) + type = attr.ib(default=None) + # all imported elements, single for + members = attr.ib(factory=list) + long_name = attr.ib(default=None) + + +class PhotoshopServerStub: """ Stub for calling function on client (Photoshop js) side. Expects that client is already connected (started when avalon menu is opened). 'self.websocketserver.call' is used as async wrapper """ + PUBLISH_ICON = '\u2117 ' + LOADED_ICON = '\u25bc' def __init__(self): self.websocketserver = WebSocketServer.get_instance() @@ -34,7 +55,7 @@ class PhotoshopServerStub(): """ Parses layer metadata from Headline field of active document Args: - layer: + Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' @@ -100,12 +145,26 @@ class PhotoshopServerStub(): return self._to_records(res) + def get_layer(self, layer_id): + """ + Returns PSItem for specific 'layer_id' or None if not found + Args: + layer_id (string): unique layer id, stored in 'uuid' field + + Returns: + (PSItem) or None + """ + layers = self.get_layers() + for layer in layers: + if str(layer.id) == str(layer_id): + return layer + def get_layers_in_layers(self, layers): """ Return all layers that belong to layers (might be groups). Args: - layers : - Returns: + layers : + Returns: """ all_layers = self.get_layers() ret = [] @@ -123,28 +182,30 @@ class PhotoshopServerStub(): def create_group(self, name): """ Create new group (eg. LayerSet) - Returns: + Returns: """ + enhanced_name = self.PUBLISH_ICON + name ret = self.websocketserver.call(self.client.call ('Photoshop.create_group', - name=name)) + name=enhanced_name)) # create group on PS is asynchronous, returns only id - layer = {"id": ret, "name": name, "group": True} - return namedtuple('Layer', layer.keys())(*layer.values()) + return PSItem(id=ret, name=name, group=True) def group_selected_layers(self, name): """ Group selected layers into new LayerSet (eg. group) Returns: (Layer) """ + enhanced_name = self.PUBLISH_ICON + name res = self.websocketserver.call(self.client.call ('Photoshop.group_selected_layers', - name=name) + name=enhanced_name) ) res = self._to_records(res) - if res: - return res.pop() + rec = res.pop() + rec.name = rec.name.replace(self.PUBLISH_ICON, '') + return rec raise ValueError("No group record returned") def get_selected_layers(self): @@ -253,6 +314,23 @@ class PhotoshopServerStub(): layers_data = json.loads(res) except json.decoder.JSONDecodeError: pass + # format of metadata changed from {} to [] because of standardization + # keep current implementation logic as its working + if not isinstance(layers_data, dict): + temp_layers_meta = {} + for layer_meta in layers_data: + layer_id = layer_meta.get("uuid") or \ + (layer_meta.get("members")[0]) + temp_layers_meta[layer_id] = layer_meta + layers_data = temp_layers_meta + else: + # legacy version of metadata + for layer_id, layer_meta in layers_data.items(): + if layer_meta.get("schema") != "avalon-core:container-2.0": + layer_meta["uuid"] = str(layer_id) + else: + layer_meta["members"] = [str(layer_id)] + return layers_data def import_smart_object(self, path, layer_name): @@ -264,11 +342,14 @@ class PhotoshopServerStub(): layer_name (str): Unique layer name to differentiate how many times same smart object was loaded """ + enhanced_name = self.LOADED_ICON + layer_name res = self.websocketserver.call(self.client.call ('Photoshop.import_smart_object', - path=path, name=layer_name)) - - return self._to_records(res).pop() + path=path, name=enhanced_name)) + rec = self._to_records(res).pop() + if rec: + rec.name = rec.name.replace(self.LOADED_ICON, '') + return rec def replace_smart_object(self, layer, path, layer_name): """ @@ -277,13 +358,14 @@ class PhotoshopServerStub(): same smart object was loaded Args: - layer (namedTuple): Layer("id":XX, "name":"YY"..). + layer (PSItem): path (str): File to import. """ + enhanced_name = self.LOADED_ICON + layer_name self.websocketserver.call(self.client.call ('Photoshop.replace_smart_object', layer_id=layer.id, - path=path, name=layer_name)) + path=path, name=enhanced_name)) def delete_layer(self, layer_id): """ @@ -295,6 +377,18 @@ class PhotoshopServerStub(): ('Photoshop.delete_layer', layer_id=layer_id)) + def rename_layer(self, layer_id, name): + """ + Renames specific layer by it's id. + Args: + layer_id (int): id of layer to delete + name (str): new name + """ + self.websocketserver.call(self.client.call + ('Photoshop.rename_layer', + layer_id=layer_id, + name=name)) + def remove_instance(self, instance_id): cleaned_data = {} @@ -313,19 +407,32 @@ class PhotoshopServerStub(): def _to_records(self, res): """ - Converts string json representation into list of named tuples for + Converts string json representation into list of PSItem for dot notation access to work. - Returns: - res(string): - json representation + Args: + res (string): valid json + Returns: + """ try: layers_data = json.loads(res) except json.decoder.JSONDecodeError: raise ValueError("Received broken JSON {}".format(res)) ret = [] - # convert to namedtuple to use dot donation - if isinstance(layers_data, dict): # TODO refactore + + # convert to AEItem to use dot donation + if isinstance(layers_data, dict): layers_data = [layers_data] for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) + # currently implemented and expected fields + item = PSItem(d.get('id'), + d.get('name'), + d.get('group'), + d.get('parents'), + d.get('visible'), + d.get('type'), + d.get('members'), + d.get('long_name')) + + ret.append(item) return ret From 628b34a3cd374b632a196630beb8d30df38cf2c9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 13:52:37 +0100 Subject: [PATCH 067/100] PS - fixed validate_naming --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 02f6fdcacb..6130e58375 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -50,8 +50,11 @@ class ValidateNaming(pyblish.api.InstancePlugin): actions = [ValidateNamingRepair] def process(self, instance): - msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + help_msg = ' Use Repair action (A) in Pyblish to fix it.' + msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], + help_msg) assert " " not in instance.data["name"], msg - msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], + help_msg) assert " " not in instance.data["subset"], msg From 2f8ecc6d82e9d0e0827e2bf8da7aed58265a6a34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 14:10:14 +0100 Subject: [PATCH 068/100] PS - fix repair validator - limit duplication of family --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 6130e58375..48f5901233 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -25,12 +25,12 @@ class ValidateNamingRepair(pyblish.api.Action): for instance in instances: self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") + name = name.replace(instance.data["family"], '') instance[0].Name = name data = stub.read(instance[0]) data["subset"] = "image" + name stub.imprint(instance[0], data) - name = name.replace(instance.data["family"], '') name = stub.PUBLISH_ICON + name stub.rename_layer(instance.data["uuid"], name) From 9e7f39957bb243502a28780d28e68ffd9761a59f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:41:02 +0100 Subject: [PATCH 069/100] removed unused import --- pype/tools/tray/pype_tray.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index c27df16276..b5fbd2598a 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -8,10 +8,6 @@ from pype.api import Logger, resources from pype.modules import TrayModulesManager, ITrayService from pype.settings.lib import get_system_settings import pype.version -try: - import configparser -except Exception: - import ConfigParser as configparser class TrayManager: From 81b081c24f78f129d7cb5f52155fabfdf8399f7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:41:59 +0100 Subject: [PATCH 070/100] removed unused working widget --- pype/tools/tray/pype_tray.py | 55 ++---------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index b5fbd2598a..27f00c8065 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -124,63 +124,14 @@ class TrayMainWindow(QtWidgets.QMainWindow): Every widget should have set this window as parent because QSystemTrayIcon widget is not allowed to be a parent of any widget. - - :param app: Qt application manages application's control flow - :type app: QtWidgets.QApplication - - .. note:: - *TrayMainWindow* has ability to show **working** widget. - Calling methods: - - ``show_working()`` - - ``hide_working()`` - .. todo:: Hide working widget if idle is too long """ def __init__(self, app): - super().__init__() + super(TrayMainWindow, self).__init__() self.app = app - self.set_working_widget() - - self.trayIcon = SystemTrayIcon(self) - self.trayIcon.show() - - def set_working_widget(self): - image_file = resources.get_resource("icons", "working.svg") - img_pix = QtGui.QPixmap(image_file) - if image_file.endswith('.svg'): - widget = QtSvg.QSvgWidget(image_file) - else: - widget = QtWidgets.QLabel() - widget.setPixmap(img_pix) - - # Set widget properties - widget.setGeometry(img_pix.rect()) - widget.setMask(img_pix.mask()) - widget.setWindowFlags( - QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint - ) - widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - - self.center_widget(widget) - self._working_widget = widget - self.helper = DragAndDropHelper(self._working_widget) - - def center_widget(self, widget): - frame_geo = widget.frameGeometry() - screen = self.app.desktop().cursor().pos() - center_point = self.app.desktop().screenGeometry( - self.app.desktop().screenNumber(screen) - ).center() - frame_geo.moveCenter(center_point) - widget.move(frame_geo.topLeft()) - - def show_working(self): - self._working_widget.show() - - def hide_working(self): - self.center_widget(self._working_widget) - self._working_widget.hide() + self.tray_widget = SystemTrayIcon(self) + self.tray_widget.show() class DragAndDropHelper: From 70fedd42bc08666f670a4e1ea11fb556c270f0b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:42:31 +0100 Subject: [PATCH 071/100] PypeTrayApplication does not change application name on windows --- pype/tools/tray/pype_tray.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 27f00c8065..48bc9fcb0c 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -177,17 +177,10 @@ class PypeTrayApplication(QtWidgets.QApplication): """Qt application manages application's control flow.""" def __init__(self): - super(self.__class__, self).__init__(sys.argv) + super(PypeTrayApplication, self).__init__(sys.argv) # Allows to close widgets without exiting app self.setQuitOnLastWindowClosed(False) - # Allow show icon istead of python icon in task bar (Windows) - if os.name == "nt": - import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - u"pype_tray" - ) - # Sets up splash splash_widget = self.set_splash() From 27c860ffe1a4819b7c305806c7594a122bb95214 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:42:50 +0100 Subject: [PATCH 072/100] removed drag and drop helper --- pype/tools/tray/pype_tray.py | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 48bc9fcb0c..8e0757ec9d 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -134,45 +134,6 @@ class TrayMainWindow(QtWidgets.QMainWindow): self.tray_widget.show() -class DragAndDropHelper: - """ Helper adds to widget drag and drop ability - - :param widget: Qt Widget where drag and drop ability will be added - """ - - def __init__(self, widget): - self.widget = widget - self.widget.mousePressEvent = self.mousePressEvent - self.widget.mouseMoveEvent = self.mouseMoveEvent - self.widget.mouseReleaseEvent = self.mouseReleaseEvent - - def mousePressEvent(self, event): - self.__mousePressPos = None - self.__mouseMovePos = None - if event.button() == QtCore.Qt.LeftButton: - self.__mousePressPos = event.globalPos() - self.__mouseMovePos = event.globalPos() - - def mouseMoveEvent(self, event): - if event.buttons() == QtCore.Qt.LeftButton: - # adjust offset from clicked point to origin of widget - currPos = self.widget.mapToGlobal( - self.widget.pos() - ) - globalPos = event.globalPos() - diff = globalPos - self.__mouseMovePos - newPos = self.widget.mapFromGlobal(currPos + diff) - self.widget.move(newPos) - self.__mouseMovePos = globalPos - - def mouseReleaseEvent(self, event): - if self.__mousePressPos is not None: - moved = event.globalPos() - self.__mousePressPos - if moved.manhattanLength() > 3: - event.ignore() - return - - class PypeTrayApplication(QtWidgets.QApplication): """Qt application manages application's control flow.""" From 29f6c1e88a7e6bdf939b1f97b0091bf369882b0b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:44:10 +0100 Subject: [PATCH 073/100] removed working svg --- pype/resources/icons/working.svg | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 pype/resources/icons/working.svg diff --git a/pype/resources/icons/working.svg b/pype/resources/icons/working.svg deleted file mode 100644 index fe73f15a31..0000000000 --- a/pype/resources/icons/working.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Working... - From eeb4529b8835735d1d659ff6a752c585c0d7e094 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:44:42 +0100 Subject: [PATCH 074/100] tray manager sends it's pointer to modules manager --- pype/modules/base.py | 4 +++- pype/tools/tray/pype_tray.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/modules/base.py b/pype/modules/base.py index 7efd00e39e..b295746b9b 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -638,8 +638,10 @@ class TrayModulesManager(ModulesManager): self.modules_by_id = {} self.modules_by_name = {} self._report = {} + self.tray_manager = None - def initialize(self, tray_menu): + def initialize(self, tray_manager, tray_menu): + self.tray_manager = tray_manager self.initialize_modules() self.tray_init() self.connect_modules() diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 8e0757ec9d..7413fe399b 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -32,7 +32,7 @@ class TrayManager: def initialize_modules(self): """Add modules to tray.""" - self.modules_manager.initialize(self.tray_widget.menu) + self.modules_manager.initialize(self, self.tray_widget.menu) # Add services if they are services_submenu = ITrayService.services_submenu(self.tray_widget.menu) From 6695a452f6496a076497353974667ca13ef7f80e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:45:05 +0100 Subject: [PATCH 075/100] tray manger has method to show tray message --- pype/tools/tray/pype_tray.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 7413fe399b..062d825c09 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -54,6 +54,26 @@ class TrayManager: # Print time report self.modules_manager.print_report() + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. + + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in miliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + args = [title, message] + kwargs = {} + if icon: + kwargs["icon"] = icon + if msecs: + kwargs["msecs"] = msecs + + self.tray_widget.showMessage(*args, **kwargs) + def _add_version_item(self): subversion = os.environ.get("PYPE_SUBVERSION") client_name = os.environ.get("PYPE_CLIENT") From e49749d307fd6b5b999bba76a04e176a28c024e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:45:21 +0100 Subject: [PATCH 076/100] tray modules can show tray message --- pype/modules/base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pype/modules/base.py b/pype/modules/base.py index b295746b9b..03a5965841 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -108,6 +108,7 @@ class ITrayModule: would do nothing. """ tray_initialized = False + _tray_manager = None @abstractmethod def tray_init(self): @@ -138,6 +139,20 @@ class ITrayModule: """ pass + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. + + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in miliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + if self._tray_manager: + self._tray_manager.show_tray_message(title, message, icon, msecs) + class ITrayAction(ITrayModule): """Implementation of Tray action. @@ -660,6 +675,7 @@ class TrayModulesManager(ModulesManager): prev_start_time = time_start for module in self.get_enabled_tray_modules(): try: + module._tray_manager = self.tray_manager module.tray_init() module.tray_initialized = True except Exception: From cfa0f4add963a04f16f1dc7d9cc5956705cdbe10 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:50:22 +0100 Subject: [PATCH 077/100] small cleanup of tray file --- pype/tools/tray/pype_tray.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 062d825c09..202b801343 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -15,13 +15,12 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - available_sourcetypes = ["python", "file"] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window - self.log = Logger().get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.module_settings = get_system_settings()["modules"] @@ -101,9 +100,9 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """ def __init__(self, parent): - self.icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.pype_icon_filepath()) - QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent) + super(SystemTrayIcon, self).__init__(icon, parent) # Store parent - QtWidgets.QMainWindow() self.parent = parent @@ -116,15 +115,15 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self.tray_man = TrayManager(self, self.parent) self.tray_man.initialize_modules() - # Catch activate event - self.activated.connect(self.on_systray_activated) + # Catch activate event for left click if not on MacOS + # - MacOS has this ability by design so menu would be doubled + if platform.system().lower() != "darwin": + self.activated.connect(self.on_systray_activated) # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) def on_systray_activated(self, reason): # show contextMenu if left click - if platform.system().lower() == "darwin": - return if reason == QtWidgets.QSystemTrayIcon.Trigger: position = QtGui.QCursor().pos() self.contextMenu().popup(position) From 5e7d057da13a2c38b2f7fa09381c20d0d2b2fa21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 15:35:47 +0100 Subject: [PATCH 078/100] tray shows small widget with basic information on version click --- pype/tools/tray/pype_tray.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 202b801343..5870db9c49 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -10,6 +10,51 @@ from pype.settings.lib import get_system_settings import pype.version +class PypeInfoWidget(QtWidgets.QWidget): + not_allowed = "N/A" + + def __init__(self, parent=None): + super(PypeInfoWidget, self).__init__(parent) + + self.setStyleSheet(style.load_stylesheet()) + + main_layout = QtWidgets.QFormLayout(self) + + if getattr(sys, "frozen", False): + version_end = "build" + else: + version_end = "code" + version_value = "{} ({})".format( + pype.version.__version__, version_end + ) + + lable_value = [ + # Pype version + ("Pype version:", version_value), + ("Pype location:", os.environ.get("PYPE_ROOT")), + + # Mongo URL + ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ] + + for label, value in lable_value: + main_layout.addRow( + label, + QtWidgets.QLabel(value or self.not_allowed) + ) + + def showEvent(self, event): + result = super(PypeInfoWidget, self).showEvent(event) + screen_center = ( + QtWidgets.QApplication.desktop().availableGeometry(self).center() + ) + self.move( + screen_center.x() - (self.width() / 2), + screen_center.y() - (self.height() / 2) + ) + return result + + class TrayManager: """Cares about context of application. @@ -20,6 +65,8 @@ class TrayManager: self.tray_widget = tray_widget self.main_window = main_window + self.pype_info_widget = None + self.log = Logger.get_logger(self.__class__.__name__) self.module_settings = get_system_settings()["modules"] @@ -85,12 +132,21 @@ class TrayManager: version_string += ", {}".format(client_name) version_action = QtWidgets.QAction(version_string, self.tray_widget) + version_action.triggered.connect(self._on_version_action) self.tray_widget.menu.addAction(version_action) self.tray_widget.menu.addSeparator() def on_exit(self): self.modules_manager.on_exit() + def _on_version_action(self): + if self.pype_info_widget is None: + self.pype_info_widget = PypeInfoWidget() + + self.pype_info_widget.show() + self.pype_info_widget.raise_() + self.pype_info_widget.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 7c9df14fc33f4957e7ddac91f643e88cb5eb8953 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 16:06:40 +0100 Subject: [PATCH 079/100] changed window icon and title --- pype/tools/tray/pype_tray.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 5870db9c49..8e6a351efd 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -18,6 +18,10 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Pype info") + main_layout = QtWidgets.QFormLayout(self) if getattr(sys, "frozen", False): From 409504927211bafa179de6140a23e922a673d4fa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 16:13:57 +0100 Subject: [PATCH 080/100] change label alignment --- pype/tools/tray/pype_tray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 8e6a351efd..b6ee033484 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -23,7 +23,7 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QFormLayout(self) - + main_layout.setLabelAlignment(QtCore.Qt.AlignRight) if getattr(sys, "frozen", False): version_end = "build" else: From a39f9d74c3909ec1f784fb86bd6fd767943b3827 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 18:12:41 +0100 Subject: [PATCH 081/100] moved pype info widget to separate file --- pype/tools/tray/pype_info_widget.py | 77 +++++++++++++++++++++++++++++ pype/tools/tray/pype_tray.py | 52 +------------------ 2 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 pype/tools/tray/pype_info_widget.py diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py new file mode 100644 index 0000000000..7c961687f1 --- /dev/null +++ b/pype/tools/tray/pype_info_widget.py @@ -0,0 +1,77 @@ +import os +import sys + +from avalon import style +from Qt import QtCore, QtGui, QtWidgets +from pype.api import Logger, resources +import pype.version + + +class PypeInfoWidget(QtWidgets.QWidget): + not_allowed = "N/A" + + def __init__(self, parent=None): + super(PypeInfoWidget, self).__init__(parent) + + self.setStyleSheet(style.load_stylesheet()) + + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Pype info") + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(self._create_pype_info_widget()) + + def _create_pype_info_widget(self): + """Create widget with information about pype application.""" + pype_root = os.environ["PYPE_ROOT"] + if getattr(sys, "frozen", False): + version_end = "build" + executable_path = sys.executable + else: + version_end = "code" + executable_path = os.path.join(pype_root, "start.py") + version_value = "{} ({})".format( + pype.version.__version__, version_end + ) + + lable_value = [ + # Pype version + ("Pype version:", version_value), + ("Pype executable:", executable_path), + ("Pype location:", pype_root), + + # Mongo URL + ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ] + + info_widget = QtWidgets.QWidget(self) + info_layout = QtWidgets.QGridLayout(info_widget) + + title_label = QtWidgets.QLabel(info_widget) + title_label.setText("Application information") + title_label.setStyleSheet("font-weight: bold;") + info_layout.addWidget(title_label, 0, 0, 1, 2) + + for label, value in lable_value: + row = info_layout.rowCount() + info_layout.addWidget( + QtWidgets.QLabel(label), row, 0, 1, 1 + ) + value_label = QtWidgets.QLabel(value or self.not_allowed) + info_layout.addWidget( + value_label, row, 1, 1, 1 + ) + return info_widget + + def showEvent(self, event): + """Center widget to center of desktop on show.""" + result = super(PypeInfoWidget, self).showEvent(event) + screen_center = ( + QtWidgets.QApplication.desktop().availableGeometry(self).center() + ) + self.move( + screen_center.x() - (self.width() / 2), + screen_center.y() - (self.height() / 2) + ) + return result diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index b6ee033484..2d37c04136 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -3,60 +3,12 @@ import sys import platform from avalon import style -from Qt import QtCore, QtGui, QtWidgets, QtSvg +from Qt import QtCore, QtGui, QtWidgets from pype.api import Logger, resources from pype.modules import TrayModulesManager, ITrayService from pype.settings.lib import get_system_settings import pype.version - - -class PypeInfoWidget(QtWidgets.QWidget): - not_allowed = "N/A" - - def __init__(self, parent=None): - super(PypeInfoWidget, self).__init__(parent) - - self.setStyleSheet(style.load_stylesheet()) - - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) - self.setWindowTitle("Pype info") - - main_layout = QtWidgets.QFormLayout(self) - main_layout.setLabelAlignment(QtCore.Qt.AlignRight) - if getattr(sys, "frozen", False): - version_end = "build" - else: - version_end = "code" - version_value = "{} ({})".format( - pype.version.__version__, version_end - ) - - lable_value = [ - # Pype version - ("Pype version:", version_value), - ("Pype location:", os.environ.get("PYPE_ROOT")), - - # Mongo URL - ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) - ] - - for label, value in lable_value: - main_layout.addRow( - label, - QtWidgets.QLabel(value or self.not_allowed) - ) - - def showEvent(self, event): - result = super(PypeInfoWidget, self).showEvent(event) - screen_center = ( - QtWidgets.QApplication.desktop().availableGeometry(self).center() - ) - self.move( - screen_center.x() - (self.width() / 2), - screen_center.y() - (self.height() / 2) - ) - return result +from .pype_info_widget import PypeInfoWidget class TrayManager: From bc4f46cf0d8496774bc7dd5b5dbe2356d6775a54 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 19:11:57 +0100 Subject: [PATCH 082/100] added environments view --- pype/tools/tray/pype_info_widget.py | 85 ++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7c961687f1..d48dcb0f9c 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -1,12 +1,61 @@ import os import sys +import json +import collections from avalon import style from Qt import QtCore, QtGui, QtWidgets -from pype.api import Logger, resources +from pype.api import resources import pype.version +class EnvironmentsView(QtWidgets.QTreeView): + def __init__(self, model, parent=None): + super(EnvironmentsView, self).__init__(parent) + self.setModel(model) + self.setIndentation(0) + self.header().setSectionResizeMode( + 0, QtWidgets.QHeaderView.ResizeToContents + ) + self.setSelectionMode(QtWidgets.QTreeView.MultiSelection) + + def get_all_as_dict(self): + pass + + def get_selection_as_dict(self): + indexes = self.selectionModel().selectedIndexes() + mapping = collections.defaultdict(dict) + for index in indexes: + row = index.row() + value = index.data(QtCore.Qt.DisplayRole) + if index.column() == 0: + key = "key" + else: + key = "value" + mapping[row][key] = value + result = {} + for item in mapping.values(): + result[item["key"]] = item["value"] + return result + + def keyPressEvent(self, event): + if ( + event.type() == QtGui.QKeyEvent.KeyPress + and event.matches(QtGui.QKeySequence.Copy) + ): + selected_dict = self.get_selection_as_dict() + selected_str = json.dumps(selected_dict, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(selected_str) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + event.accept() + else: + return super(EnvironmentsView, self).keyPressEvent(event) + + class PypeInfoWidget(QtWidgets.QWidget): not_allowed = "N/A" @@ -21,6 +70,34 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(self._create_pype_info_widget()) + main_layout.addWidget(self._create_environ_widget()) + + def _create_environ_widget(self): + env_widget = QtWidgets.QWidget(self) + + env_label_widget = QtWidgets.QLabel("Environments", env_widget) + env_label_widget.setStyleSheet("font-weight: bold;") + + env_model = QtGui.QStandardItemModel() + + env = os.environ.copy() + keys = [] + values = [] + for key in sorted(env.keys()): + keys.append(QtGui.QStandardItem(key)) + values.append(QtGui.QStandardItem(env[key])) + + env_model.appendColumn(keys) + env_model.appendColumn(values) + env_model.setHorizontalHeaderLabels(["Key", "Value"]) + + env_view = EnvironmentsView(env_model, env_widget) + + env_layout = QtWidgets.QVBoxLayout(env_widget) + env_layout.addWidget(env_label_widget) + env_layout.addWidget(env_view) + + return env_widget def _create_pype_info_widget(self): """Create widget with information about pype application.""" @@ -47,6 +124,9 @@ class PypeInfoWidget(QtWidgets.QWidget): info_widget = QtWidgets.QWidget(self) info_layout = QtWidgets.QGridLayout(info_widget) + # Add spacer to 3rd column + info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2) + info_layout.setColumnStretch(2, 1) title_label = QtWidgets.QLabel(info_widget) title_label.setText("Application information") @@ -59,6 +139,9 @@ class PypeInfoWidget(QtWidgets.QWidget): QtWidgets.QLabel(label), row, 0, 1, 1 ) value_label = QtWidgets.QLabel(value or self.not_allowed) + value_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse + ) info_layout.addWidget( value_label, row, 1, 1, 1 ) From 7c1118c31af4c33bc31fb76af809675139b64b21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 10:40:46 +0100 Subject: [PATCH 083/100] added local settings view --- pype/tools/tray/pype_info_widget.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index d48dcb0f9c..0a105ede9f 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -7,6 +7,7 @@ from avalon import style from Qt import QtCore, QtGui, QtWidgets from pype.api import resources import pype.version +from pype.settings.lib import get_local_settings class EnvironmentsView(QtWidgets.QTreeView): @@ -69,8 +70,29 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(self._create_pype_info_widget()) - main_layout.addWidget(self._create_environ_widget()) + main_layout.addWidget(self._create_pype_info_widget(), 0) + main_layout.addWidget(self._create_local_settings_widget(), 0) + main_layout.addWidget(self._create_environ_widget(), 1) + + def _create_local_settings_widget(self): + local_settings = get_local_settings() + + local_settings_widget = QtWidgets.QWidget(self) + + label_widget = QtWidgets.QLabel( + "Local settings", local_settings_widget + ) + label_widget.setStyleSheet("font-weight: bold;") + settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) + settings_input.setReadOnly(True) + settings_input.setMinimumHeight(20) + settings_input.setPlainText(json.dumps(local_settings, indent=4)) + + local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) + local_settings_layout.addWidget(label_widget) + local_settings_layout.addWidget(settings_input) + + return local_settings_widget def _create_environ_widget(self): env_widget = QtWidgets.QWidget(self) From 0f02db0997acef1935c070bc517f8728f59982e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:42:45 +0100 Subject: [PATCH 084/100] removed minimum height --- pype/tools/tray/pype_info_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 0a105ede9f..bd9bc8d3ee 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -85,7 +85,6 @@ class PypeInfoWidget(QtWidgets.QWidget): label_widget.setStyleSheet("font-weight: bold;") settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) settings_input.setReadOnly(True) - settings_input.setMinimumHeight(20) settings_input.setPlainText(json.dumps(local_settings, indent=4)) local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) From 4ed74637b2568519dde90e423a395789695804f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:55:40 +0100 Subject: [PATCH 085/100] envs and local settings are collapsible --- pype/tools/tray/pype_info_widget.py | 102 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index bd9bc8d3ee..b958aa637b 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -57,6 +57,88 @@ class EnvironmentsView(QtWidgets.QTreeView): return super(EnvironmentsView, self).keyPressEvent(event) +class ClickableWidget(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ClickableWidget, self).mouseReleaseEvent(event) + + +class CollapsibleWidget(QtWidgets.QWidget): + def __init__(self, label, parent): + super(CollapsibleWidget, self).__init__(parent) + + self.content_widget = None + + top_part = ClickableWidget(parent=self) + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + label_widget = QtWidgets.QLabel(label, parent=top_part) + spacer_widget = QtWidgets.QWidget(top_part) + + top_part_layout = QtWidgets.QHBoxLayout(top_part) + top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.addWidget(button_toggle) + top_part_layout.addWidget(label_widget) + top_part_layout.addWidget(spacer_widget, 1) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.button_toggle = button_toggle + self.label_widget = label_widget + + top_part.clicked.connect(self._top_part_clicked) + self.button_toggle.clicked.connect(self._btn_clicked) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.setAlignment(QtCore.Qt.AlignTop) + main_layout.addWidget(top_part) + + self.main_layout = main_layout + + def set_content_widget(self, content_widget): + content_widget.setVisible(self.button_toggle.isChecked()) + self.main_layout.addWidget(content_widget) + self.content_widget = content_widget + + def _btn_clicked(self): + self.toggle_content(self.button_toggle.isChecked()) + + def _top_part_clicked(self): + self.toggle_content() + + def toggle_content(self, *args): + if len(args) > 0: + checked = args[0] + else: + checked = not self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + if self.content_widget: + self.content_widget.setVisible(checked) + self.parent().updateGeometry() + + def resizeEvent(self, event): + super(CollapsibleWidget, self).resizeEvent(event) + if self.content_widget: + self.content_widget.updateGeometry() + + class PypeInfoWidget(QtWidgets.QWidget): not_allowed = "N/A" @@ -70,6 +152,7 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -77,27 +160,18 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_local_settings_widget(self): local_settings = get_local_settings() - local_settings_widget = QtWidgets.QWidget(self) + local_settings_widget = CollapsibleWidget("Local settings", self) - label_widget = QtWidgets.QLabel( - "Local settings", local_settings_widget - ) - label_widget.setStyleSheet("font-weight: bold;") settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) settings_input.setReadOnly(True) settings_input.setPlainText(json.dumps(local_settings, indent=4)) - local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) - local_settings_layout.addWidget(label_widget) - local_settings_layout.addWidget(settings_input) + local_settings_widget.set_content_widget(settings_input) return local_settings_widget def _create_environ_widget(self): - env_widget = QtWidgets.QWidget(self) - - env_label_widget = QtWidgets.QLabel("Environments", env_widget) - env_label_widget.setStyleSheet("font-weight: bold;") + env_widget = CollapsibleWidget("Environments", self) env_model = QtGui.QStandardItemModel() @@ -114,9 +188,7 @@ class PypeInfoWidget(QtWidgets.QWidget): env_view = EnvironmentsView(env_model, env_widget) - env_layout = QtWidgets.QVBoxLayout(env_widget) - env_layout.addWidget(env_label_widget) - env_layout.addWidget(env_view) + env_widget.set_content_widget(env_view) return env_widget From b9e84d078f0dfc807fd929cb94fa14b496b7ded4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:56:45 +0100 Subject: [PATCH 086/100] don't center widget --- pype/tools/tray/pype_info_widget.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index b958aa637b..7bce740305 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -239,15 +239,3 @@ class PypeInfoWidget(QtWidgets.QWidget): value_label, row, 1, 1, 1 ) return info_widget - - def showEvent(self, event): - """Center widget to center of desktop on show.""" - result = super(PypeInfoWidget, self).showEvent(event) - screen_center = ( - QtWidgets.QApplication.desktop().availableGeometry(self).center() - ) - self.move( - screen_center.x() - (self.width() / 2), - screen_center.y() - (self.height() / 2) - ) - return result From 17a499225fc11f07b80c23d24c684cb8e0a6d81a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 12:00:29 +0100 Subject: [PATCH 087/100] minor modifications --- pype/tools/tray/pype_info_widget.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7bce740305..7915e20be1 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -85,7 +85,7 @@ class CollapsibleWidget(QtWidgets.QWidget): spacer_widget = QtWidgets.QWidget(top_part) top_part_layout = QtWidgets.QHBoxLayout(top_part) - top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.setContentsMargins(0, 0, 0, 5) top_part_layout.addWidget(button_toggle) top_part_layout.addWidget(label_widget) top_part_layout.addWidget(spacer_widget, 1) @@ -154,9 +154,18 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) + def _create_separator(self): + separator_widget = QtWidgets.QWidget(self) + separator_widget.setStyleSheet("background: #222222;") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + return separator_widget + def _create_local_settings_widget(self): local_settings = get_local_settings() From 7ad4567534b815cd6a1d06a8dbc3ff864926d5d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 13:20:45 +0100 Subject: [PATCH 088/100] changed selection mode --- pype/tools/tray/pype_info_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7915e20be1..0aa618c42b 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -18,7 +18,7 @@ class EnvironmentsView(QtWidgets.QTreeView): self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) - self.setSelectionMode(QtWidgets.QTreeView.MultiSelection) + self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) def get_all_as_dict(self): pass From 44e03989d452d333e164229dbd133f9e81d547bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 20:01:07 +0100 Subject: [PATCH 089/100] added pype info getters to pype.lib --- pype/lib/pype_info.py | 73 +++++++++++++++++++++++++++++ pype/tools/tray/pype_info_widget.py | 29 +++++------- 2 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 pype/lib/pype_info.py diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py new file mode 100644 index 0000000000..8db43a654f --- /dev/null +++ b/pype/lib/pype_info.py @@ -0,0 +1,73 @@ +import os +import sys +import json +import datetime +import platform +import getpass +import socket + +import pype.version +from pype.settings.lib import get_local_settings +from .execute import get_pype_execute_args +from .local_settings import get_local_site_id + + +def get_pype_version(): + return pype.version.__version__ + + +def get_pype_info(): + executable_args = get_pype_execute_args() + if len(executable_args) == 1: + version_type = "build" + else: + version_type = "code" + + return { + "version": get_pype_version(), + "version_type": version_type, + "executable": executable_args[-1], + "pype_root": os.environ["PYPE_ROOT"], + "mongo_url": os.environ["PYPE_MONGO"] + } + + +def get_workstation_info(): + host_name = socket.gethostname() + try: + host_ip = socket.gethostbyname(host_name) + except socket.gaierror: + host_ip = "127.0.0.1" + + return { + "hostname": host_name, + "hostip": host_ip, + "username": getpass.getuser(), + "system_name": platform.system(), + "local_id": get_local_site_id() + } + + +def get_all_data(): + return { + "pype": get_pype_info(), + "workstation": get_workstation_info(), + "env": os.environ.copy(), + "local_settings": get_local_settings() + } + + +def extract_pype_info_to_file(dirpath): + filename = "{}_{}_{}.json".format( + get_pype_version(), + get_local_site_id(), + datetime.datetime.now().strftime("%y%m%d%H%M%S") + ) + filepath = os.path.join(dirpath, filename) + data = get_all_data() + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + with open(filepath, "w") as file_stream: + json.dump(data, file_stream) + return filepath diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 0aa618c42b..d08cd404cd 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -8,6 +8,11 @@ from Qt import QtCore, QtGui, QtWidgets from pype.api import resources import pype.version from pype.settings.lib import get_local_settings +from pype.lib.pype_info import ( + get_pype_info, + get_workstation_info, + extract_pype_info_to_file +) class EnvironmentsView(QtWidgets.QTreeView): @@ -20,9 +25,6 @@ class EnvironmentsView(QtWidgets.QTreeView): ) self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) - def get_all_as_dict(self): - pass - def get_selection_as_dict(self): indexes = self.selectionModel().selectedIndexes() mapping = collections.defaultdict(dict) @@ -203,25 +205,20 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_pype_info_widget(self): """Create widget with information about pype application.""" - pype_root = os.environ["PYPE_ROOT"] - if getattr(sys, "frozen", False): - version_end = "build" - executable_path = sys.executable - else: - version_end = "code" - executable_path = os.path.join(pype_root, "start.py") - version_value = "{} ({})".format( - pype.version.__version__, version_end - ) + pype_info = get_pype_info() + version_value = "{} ({})".format( + pype_info["version"], + pype_info["version_type"] + ) lable_value = [ # Pype version ("Pype version:", version_value), - ("Pype executable:", executable_path), - ("Pype location:", pype_root), + ("Pype executable:", pype_info["executable"]), + ("Pype location:", pype_info["pype_root"]), # Mongo URL - ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ("Pype Mongo URL:", pype_info["mongo_url"]) ] info_widget = QtWidgets.QWidget(self) From 9cc95a9ac36ddf31c1e6a36534f3f38a1f05aefe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 20:18:47 +0100 Subject: [PATCH 090/100] fixed environments editability --- pype/tools/tray/pype_info_widget.py | 45 +++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index d08cd404cd..6e638616e5 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -15,11 +15,39 @@ from pype.lib.pype_info import ( ) +class EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate): + def createEditor(self, parent, option, index): + edit_widget = QtWidgets.QLineEdit(parent) + edit_widget.setReadOnly(True) + return edit_widget + + class EnvironmentsView(QtWidgets.QTreeView): - def __init__(self, model, parent=None): + def __init__(self, parent=None): super(EnvironmentsView, self).__init__(parent) + + model = QtGui.QStandardItemModel() + + env = os.environ.copy() + keys = [] + values = [] + for key in sorted(env.keys()): + key_item = QtGui.QStandardItem(key) + key_item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) + keys.append(key_item) + values.append(QtGui.QStandardItem(env[key])) + + model.appendColumn(keys) + model.appendColumn(values) + model.setHorizontalHeaderLabels(["Key", "Value"]) + self.setModel(model) self.setIndentation(0) + delegate = EnvironmentValueDelegate(self) + self.setItemDelegateForColumn(1, delegate) self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) @@ -184,20 +212,7 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_environ_widget(self): env_widget = CollapsibleWidget("Environments", self) - env_model = QtGui.QStandardItemModel() - - env = os.environ.copy() - keys = [] - values = [] - for key in sorted(env.keys()): - keys.append(QtGui.QStandardItem(key)) - values.append(QtGui.QStandardItem(env[key])) - - env_model.appendColumn(keys) - env_model.appendColumn(values) - env_model.setHorizontalHeaderLabels(["Key", "Value"]) - - env_view = EnvironmentsView(env_model, env_widget) + env_view = EnvironmentsView(env_widget) env_widget.set_content_widget(env_view) From 276a7df3157c172176f6938d84e5e6177502813d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 09:45:33 +0100 Subject: [PATCH 091/100] added workstation info --- pype/tools/tray/pype_info_widget.py | 91 ++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 6e638616e5..f3ad668429 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -170,7 +170,7 @@ class CollapsibleWidget(QtWidgets.QWidget): class PypeInfoWidget(QtWidgets.QWidget): - not_allowed = "N/A" + not_applicable = "N/A" def __init__(self, parent=None): super(PypeInfoWidget, self).__init__(parent) @@ -185,6 +185,8 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) main_layout.addWidget(self._create_separator(), 0) + main_layout.addWidget(self._create_workstation_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -196,6 +198,56 @@ class PypeInfoWidget(QtWidgets.QWidget): separator_widget.setMaximumHeight(2) return separator_widget + def _create_workstation_widget(self): + key_label_mapping = { + "system_name": "System:", + "local_id": "Local ID:", + "username": "Username:", + "hostname": "Hostname:", + "hostip": "Host IP:" + } + keys_order = [ + "system_name", + "local_id", + "username", + "hostname", + "hostip" + ] + workstation_info = get_workstation_info() + for key in workstation_info.keys(): + if key not in keys_order: + keys_order.append(key) + + wokstation_info_widget = CollapsibleWidget("Workstation info", self) + + info_widget = QtWidgets.QWidget(self) + info_layout = QtWidgets.QGridLayout(info_widget) + # Add spacer to 3rd column + info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2) + info_layout.setColumnStretch(2, 1) + + for key in keys_order: + if key not in workstation_info: + continue + + label = key_label_mapping.get(key, key) + value = workstation_info[key] + row = info_layout.rowCount() + info_layout.addWidget( + QtWidgets.QLabel(label), row, 0, 1, 1 + ) + value_label = QtWidgets.QLabel(value) + value_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse + ) + info_layout.addWidget( + value_label, row, 1, 1, 1 + ) + + wokstation_info_widget.set_content_widget(info_widget) + + return wokstation_info_widget + def _create_local_settings_widget(self): local_settings = get_local_settings() @@ -221,21 +273,28 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_pype_info_widget(self): """Create widget with information about pype application.""" + # Get pype info data pype_info = get_pype_info() + # Modify version key/values version_value = "{} ({})".format( - pype_info["version"], - pype_info["version_type"] + pype_info.pop("version", self.not_applicable), + pype_info.pop("version_type", self.not_applicable) ) - lable_value = [ - # Pype version - ("Pype version:", version_value), - ("Pype executable:", pype_info["executable"]), - ("Pype location:", pype_info["pype_root"]), - - # Mongo URL - ("Pype Mongo URL:", pype_info["mongo_url"]) - ] + pype_info["version_value"] = version_value + # Prepare lable mapping + key_label_mapping = { + "version_value": "Pype version:", + "executable": "Pype executable:", + "pype_root": "Pype location:", + "mongo_url": "Pype Mongo URL:" + } + # Prepare keys order + keys_order = ["version_value", "executable", "pype_root", "mongo_url"] + for key in pype_info.keys(): + if key not in keys_order: + keys_order.append(key) + # Create widgets info_widget = QtWidgets.QWidget(self) info_layout = QtWidgets.QGridLayout(info_widget) # Add spacer to 3rd column @@ -247,12 +306,16 @@ class PypeInfoWidget(QtWidgets.QWidget): title_label.setStyleSheet("font-weight: bold;") info_layout.addWidget(title_label, 0, 0, 1, 2) - for label, value in lable_value: + for key in keys_order: + if key not in pype_info: + continue + value = pype_info[key] + label = key_label_mapping.get(key, key) row = info_layout.rowCount() info_layout.addWidget( QtWidgets.QLabel(label), row, 0, 1, 1 ) - value_label = QtWidgets.QLabel(value or self.not_allowed) + value_label = QtWidgets.QLabel(value) value_label.setTextInteractionFlags( QtCore.Qt.TextSelectableByMouse ) From 841c1b3918c9a7cbef8145eddb2ee1171cb26abf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:14:54 +0100 Subject: [PATCH 092/100] added buttons to copy or extract information --- pype/lib/pype_info.py | 2 +- pype/tools/tray/pype_info_widget.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index 8db43a654f..b52fc71635 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -69,5 +69,5 @@ def extract_pype_info_to_file(dirpath): os.makedirs(dirpath) with open(filepath, "w") as file_stream: - json.dump(data, file_stream) + json.dump(data, file_stream, indent=4) return filepath diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index f3ad668429..691bc12db5 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -9,6 +9,7 @@ from pype.api import resources import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( + get_all_data, get_pype_info, get_workstation_info, extract_pype_info_to_file @@ -190,6 +191,58 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) + main_layout.addWidget(self._create_btns_section(), 0) + + def _create_btns_section(self): + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + + copy_to_clipboard_btn = QtWidgets.QPushButton( + "Copy to clipboard", btns_widget + ) + export_to_file_btn = QtWidgets.QPushButton( + "Export", btns_widget + ) + btns_layout.addWidget(QtWidgets.QWidget(btns_widget)) + btns_layout.addWidget(copy_to_clipboard_btn) + btns_layout.addWidget(export_to_file_btn) + + copy_to_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) + export_to_file_btn.clicked.connect(self._on_export_to_file) + + return btns_widget + + def _on_export_to_file(self): + dst_dir_path = QtWidgets.QFileDialog.getExistingDirectory( + self, + "Choose directory", + os.path.expanduser("~"), + QtWidgets.QFileDialog.ShowDirsOnly + ) + if not dst_dir_path or not os.path.exists(dst_dir_path): + return + + filepath = extract_pype_info_to_file(dst_dir_path) + title = "Extraction done" + message = "Extraction is done. Destination filepath is \"{}\"".format( + filepath.replace("\\", "/") + ) + dialog = QtWidgets.QMessageBox(self) + dialog.setIcon(QtWidgets.QMessageBox.NoIcon) + dialog.setWindowTitle(title) + dialog.setText(message) + dialog.exec_() + + def _on_copy_to_clipboard(self): + all_data = get_all_data() + all_data_str = json.dumps(all_data, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(all_data_str) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) def _create_separator(self): separator_widget = QtWidgets.QWidget(self) From 24c49a7cc3e8e4101c84e64d05cf0918a6318899 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:21:44 +0100 Subject: [PATCH 093/100] added some docstrings --- pype/lib/pype_info.py | 19 +++++++++++++++++-- pype/tools/tray/pype_info_widget.py | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index b52fc71635..4c1bbccba0 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -13,10 +13,12 @@ from .local_settings import get_local_site_id def get_pype_version(): + """Version of pype that is currently used.""" return pype.version.__version__ def get_pype_info(): + """Information about currently used Pype process.""" executable_args = get_pype_execute_args() if len(executable_args) == 1: version_type = "build" @@ -33,6 +35,7 @@ def get_pype_info(): def get_workstation_info(): + """Basic information about workstation.""" host_name = socket.gethostname() try: host_ip = socket.gethostbyname(host_name) @@ -48,7 +51,8 @@ def get_workstation_info(): } -def get_all_data(): +def get_all_current_info(): + """All information about current process in one dictionary.""" return { "pype": get_pype_info(), "workstation": get_workstation_info(), @@ -58,13 +62,24 @@ def get_all_data(): def extract_pype_info_to_file(dirpath): + """Extract all current info to a file. + + It is possible to define onpy directory path. Filename is concatenated with + pype version, workstation site id and timestamp. + + Args: + dirpath (str): Path to directory where file will be stored. + + Returns: + filepath (str): Full path to file where data were extracted. + """ filename = "{}_{}_{}.json".format( get_pype_version(), get_local_site_id(), datetime.datetime.now().strftime("%y%m%d%H%M%S") ) filepath = os.path.join(dirpath, filename) - data = get_all_data() + data = get_all_current_info() if not os.path.exists(dirpath): os.makedirs(dirpath) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 691bc12db5..fcd820b0b2 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -9,7 +9,7 @@ from pype.api import resources import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( - get_all_data, + get_all_current_info, get_pype_info, get_workstation_info, extract_pype_info_to_file @@ -235,7 +235,7 @@ class PypeInfoWidget(QtWidgets.QWidget): dialog.exec_() def _on_copy_to_clipboard(self): - all_data = get_all_data() + all_data = get_all_current_info() all_data_str = json.dumps(all_data, indent=4) mime_data = QtCore.QMimeData() From 75fd04ca2b36cab4f183c80d6f39b1c0d3c09469 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:30:05 +0100 Subject: [PATCH 094/100] fixed buttons expandings --- pype/tools/tray/pype_info_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index fcd820b0b2..1a3fba0277 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -204,7 +204,7 @@ class PypeInfoWidget(QtWidgets.QWidget): export_to_file_btn = QtWidgets.QPushButton( "Export", btns_widget ) - btns_layout.addWidget(QtWidgets.QWidget(btns_widget)) + btns_layout.addWidget(QtWidgets.QWidget(btns_widget), 1) btns_layout.addWidget(copy_to_clipboard_btn) btns_layout.addWidget(export_to_file_btn) From aa62be3662f2c0bcec8346b839dfa6aaeaa279aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:54:50 +0100 Subject: [PATCH 095/100] added ability to show separated paths --- pype/tools/tray/pype_info_widget.py | 43 +++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 1a3fba0277..dd59bc03f2 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -1,5 +1,4 @@ import os -import sys import json import collections @@ -15,6 +14,8 @@ from pype.lib.pype_info import ( extract_pype_info_to_file ) +IS_MAIN_ROLE = QtCore.Qt.UserRole + class EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): @@ -38,17 +39,36 @@ class EnvironmentsView(QtWidgets.QTreeView): QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled ) + key_item.setData(True, IS_MAIN_ROLE) keys.append(key_item) - values.append(QtGui.QStandardItem(env[key])) + + value = env[key] + value_item = QtGui.QStandardItem(value) + value_item.setData(True, IS_MAIN_ROLE) + values.append(value_item) + + value_parts = [ + part + for part in value.split(os.pathsep) if part + ] + if len(value_parts) < 2: + continue + + sub_parts = [] + for part_value in value_parts: + part_item = QtGui.QStandardItem(part_value) + part_item.setData(False, IS_MAIN_ROLE) + sub_parts.append(part_item) + key_item.appendRows(sub_parts) model.appendColumn(keys) model.appendColumn(values) model.setHorizontalHeaderLabels(["Key", "Value"]) self.setModel(model) - self.setIndentation(0) + # self.setIndentation(0) delegate = EnvironmentValueDelegate(self) - self.setItemDelegateForColumn(1, delegate) + self.setItemDelegate(delegate) self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) @@ -56,17 +76,22 @@ class EnvironmentsView(QtWidgets.QTreeView): def get_selection_as_dict(self): indexes = self.selectionModel().selectedIndexes() - mapping = collections.defaultdict(dict) + + main_mapping = collections.defaultdict(dict) for index in indexes: + is_main = index.data(IS_MAIN_ROLE) + if not is_main: + continue row = index.row() value = index.data(QtCore.Qt.DisplayRole) if index.column() == 0: key = "key" else: key = "value" - mapping[row][key] = value + main_mapping[row][key] = value + result = {} - for item in mapping.values(): + for item in main_mapping.values(): result[item["key"]] = item["value"] return result @@ -75,8 +100,8 @@ class EnvironmentsView(QtWidgets.QTreeView): event.type() == QtGui.QKeyEvent.KeyPress and event.matches(QtGui.QKeySequence.Copy) ): - selected_dict = self.get_selection_as_dict() - selected_str = json.dumps(selected_dict, indent=4) + selected_data = self.get_selection_as_dict() + selected_str = json.dumps(selected_data, indent=4) mime_data = QtCore.QMimeData() mime_data.setText(selected_str) From 208d8301ed0df2a31848403e25731276baf0a449 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:57:31 +0100 Subject: [PATCH 096/100] imports cleanup --- pype/lib/pype_info.py | 1 - pype/tools/tray/pype_info_widget.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index 4c1bbccba0..cbcc5811a0 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -1,5 +1,4 @@ import os -import sys import json import datetime import platform diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index dd59bc03f2..a4c52eb1d0 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -5,7 +5,6 @@ import collections from avalon import style from Qt import QtCore, QtGui, QtWidgets from pype.api import resources -import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( get_all_current_info, From acb18c45df1ec21348681267c6ca420fedceac3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 12:53:49 +0100 Subject: [PATCH 097/100] PS - fix create multiple groups instances --- .../websocket_server/stubs/photoshop_server_stub.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 79a486a20c..5409120a65 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -224,11 +224,10 @@ class PhotoshopServerStub: layers: Returns: None """ - layer_ids = [layer.id for layer in layers] - + layers_id = [str(lay.id) for lay in layers] self.websocketserver.call(self.client.call - ('Photoshop.get_layers', - layers=layer_ids) + ('Photoshop.select_layers', + layers=json.dumps(layers_id)) ) def get_active_document_full_name(self): From 330f3cc4d3a98359f6b7a963ab5ff91afbe93194 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 12:55:29 +0100 Subject: [PATCH 098/100] there is no need of overriding taskChanged event only add new callback --- pype/hosts/maya/api/__init__.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/pype/hosts/maya/api/__init__.py b/pype/hosts/maya/api/__init__.py index 27e15e0b25..ea100dda0f 100644 --- a/pype/hosts/maya/api/__init__.py +++ b/pype/hosts/maya/api/__init__.py @@ -7,7 +7,7 @@ from maya import utils, cmds from avalon import api as avalon from avalon import pipeline from avalon.maya import suspended_refresh -from avalon.maya.pipeline import IS_HEADLESS, _on_task_changed +from avalon.maya.pipeline import IS_HEADLESS from avalon.tools import workfiles from pyblish import api as pyblish from pype.lib import any_outdated @@ -45,9 +45,7 @@ def install(): avalon.on("open", on_open) avalon.on("new", on_new) avalon.before("save", on_before_save) - - log.info("Overriding existing event 'taskChanged'") - override_event("taskChanged", on_task_changed) + avalon.on("taskChanged", on_task_changed) log.info("Setting default family states for loader..") avalon.data["familiesStateToggled"] = ["imagesequence"] @@ -61,24 +59,6 @@ def uninstall(): menu.uninstall() -def override_event(event, callback): - """ - Override existing event callback - Args: - event (str): name of the event - callback (function): callback to be triggered - - Returns: - None - - """ - - ref = weakref.WeakSet() - ref.add(callback) - - pipeline._registered_event_handlers[event] = ref - - def on_init(_): avalon.logger.info("Running callback on init..") @@ -215,7 +195,6 @@ def on_new(_): def on_task_changed(*args): """Wrapped function of app initialize and maya's on task changed""" # Run - _on_task_changed() with suspended_refresh(): lib.set_context_settings() lib.update_content_on_context_change() From 050429948a4c268a7598c9715acbe0d0cef001d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 12:55:43 +0100 Subject: [PATCH 099/100] fixed widgets import in maya's implementation --- pype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/api/lib.py b/pype/hosts/maya/api/lib.py index 56621949b3..44f1577f7f 100644 --- a/pype/hosts/maya/api/lib.py +++ b/pype/hosts/maya/api/lib.py @@ -2677,7 +2677,7 @@ def update_content_on_context_change(): def show_message(title, msg): from avalon.vendor.Qt import QtWidgets - from ...widgets import message_window + from pype.widgets import message_window # Find maya main window top_level_widgets = {w.objectName(): w for w in From ea678212f39631fd7d7eb498eb4f701be8a49bd2 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Mar 2021 17:27:39 +0100 Subject: [PATCH 100/100] pull latest develop --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index eae14f2960..8d3364dc8a 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit eae14f2960c4ccf2f0211e0726e88563129c0296 +Subproject commit 8d3364dc8ae73a33726ba3279ff75adff73c6239