From ebcf9600b0e98d5c3478420bd694ff04634940da Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:43 -0400 Subject: [PATCH 01/17] Move Render Review into Blender Kitsu Add-On --- .../addons/blender_kitsu/.gitignore | 115 ++- .../addons/blender_kitsu/README.md | 31 + .../addons/blender_kitsu/__init__.py | 3 + scripts-blender/addons/blender_kitsu/prefs.py | 76 ++ .../blender_kitsu/render_review/README.md | 0 .../render_review/__init__.py | 27 +- .../render_review/checksqe.py | 0 .../{ => blender_kitsu}/render_review/draw.py | 0 .../render_review/exception.py | 0 .../render_review/kitsu.py | 11 +- .../{ => blender_kitsu}/render_review/ops.py | 76 +- .../render_review/opsdata.py | 66 +- .../render_review/props.py | 5 +- .../{ => blender_kitsu}/render_review/ui.py | 28 +- .../{ => blender_kitsu}/render_review/util.py | 2 +- .../{ => blender_kitsu}/render_review/vars.py | 0 .../addons/render_review/.gitignore | 112 --- .../addons/render_review/CHANGELOG.md | 34 - scripts-blender/addons/render_review/LICENSE | 674 ------------------ .../addons/render_review/README.md | 34 - scripts-blender/addons/render_review/log.py | 33 - scripts-blender/addons/render_review/prefs.py | 259 ------- 22 files changed, 292 insertions(+), 1294 deletions(-) create mode 100644 scripts-blender/addons/blender_kitsu/render_review/README.md rename scripts-blender/addons/{ => blender_kitsu}/render_review/__init__.py (70%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/checksqe.py (100%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/draw.py (100%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/exception.py (100%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/kitsu.py (92%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/ops.py (94%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/opsdata.py (87%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/props.py (96%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/ui.py (88%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/util.py (97%) rename scripts-blender/addons/{ => blender_kitsu}/render_review/vars.py (100%) delete mode 100644 scripts-blender/addons/render_review/.gitignore delete mode 100644 scripts-blender/addons/render_review/CHANGELOG.md delete mode 100644 scripts-blender/addons/render_review/LICENSE delete mode 100644 scripts-blender/addons/render_review/README.md delete mode 100644 scripts-blender/addons/render_review/log.py delete mode 100644 scripts-blender/addons/render_review/prefs.py diff --git a/scripts-blender/addons/blender_kitsu/.gitignore b/scripts-blender/addons/blender_kitsu/.gitignore index 2621fabc..dea180d8 100644 --- a/scripts-blender/addons/blender_kitsu/.gitignore +++ b/scripts-blender/addons/blender_kitsu/.gitignore @@ -1 +1,114 @@ -*.blend1 \ No newline at end of file +*.blend1 + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +.venv* +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# IDE settings +.vscode/ + +# utility bat files: +*jump_in_venv.bat + +#local tests +tests/local* \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/README.md b/scripts-blender/addons/blender_kitsu/README.md index fc1262eb..b78c0aa8 100644 --- a/scripts-blender/addons/blender_kitsu/README.md +++ b/scripts-blender/addons/blender_kitsu/README.md @@ -21,6 +21,7 @@ blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It - [Lookdev Tools](#lookdev-tools) - [Error System](#error-system) - [Shot Builder](#shot-builder) + - [Render Review](#render-review) - [Development](#development) - [Update Dependencies](#update-dependencies) - [Troubleshoot](#troubleshoot) @@ -428,6 +429,36 @@ Features ``` +## Render Review +Blender Add-on to review renders from Flamenco with the Sequence Editor + +### Installation +1. Download [latest release](../addons/overview) +2. Launch Blender, navigate to `Edit > Preferences` select `Addons` and then `Install`, +3. Navigate to the downloaded add-on and select `Install Add-on` + +After install you need to configure the addon in the addon preferences. + +### Before you get started + +This addon requires a specific folder structure of the rendering pipeline. This structure is defined by Flamenco + +If you have a different folder structure the addon might not work as +expected. + +### Features +- Quickly load all versions of a shot or a whole sequence that was rendered with Flamenco in to the Sequence Editor +- Inspect EXR's of selected sequence strip with one click +- Approve render which copies data from the farm_output to the shot_frames folder +- Push a render to the edit which uses the existing .mp4 preview or creates it with ffmpeg +and copies it over to the shot_preview folder with automatic versioning incrementation +- Creation of metadata.json files on approving renders and pushing renders to edit to keep track where a file came from +- Connection to `blender-kitsu` addon, that can be enabled and extends the functionality of some operators + +### Links + +Flamenco Doc + ## Development ### Update Dependencies diff --git a/scripts-blender/addons/blender_kitsu/__init__.py b/scripts-blender/addons/blender_kitsu/__init__.py index 8fe66dcc..0169dc34 100644 --- a/scripts-blender/addons/blender_kitsu/__init__.py +++ b/scripts-blender/addons/blender_kitsu/__init__.py @@ -24,6 +24,7 @@ dependencies.preload_modules() from . import ( shot_builder, + render_review, lookdev, bkglobals, types, @@ -100,6 +101,7 @@ def register(): playblast.register() anim.register() shot_builder.register() + render_review.register() edit.register() LoggerLevelManager.configure_levels() @@ -119,6 +121,7 @@ def unregister(): lookdev.unregister() playblast.unregister() shot_builder.unregister() + render_review.unregister() edit.unregister() LoggerLevelManager.restore_levels() diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index 5bbdeafb..ac330794 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -465,6 +465,40 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): tasks: bpy.props.CollectionProperty(type=KITSU_task) + #################### + # Render Review + #################### + + farm_output_dir: bpy.props.StringProperty( # type: ignore + name="Farm Output Directory", + description="Should point to: /render/sprites/farm_output", + default="/render/sprites/farm_output", + subtype="DIR_PATH", + ) + + shot_name_filter: bpy.props.StringProperty( # type: ignore + name="Shot Name Filter", + description="Shot name must include this string, otherwise it will be ignored", + default="", + ) + use_video: bpy.props.BoolProperty( + name="Use Video", + description="Load video versions of renders rather than image sequences for faster playback", + ) + use_video_latest_only: bpy.props.BoolProperty( + default=True, + name="Latest Only", + description="Only load video files for the latest versions by default, to avoid running out of memory and crashing", + ) + + versions_max_count: bpy.props.IntProperty( + name="Max Versions", + description="Desired number of versions to load for each shot", + min=1, + max=128, + default=32, + ) + def draw(self, context: bpy.types.Context) -> None: layout = self.layout layout.use_property_split = True @@ -569,6 +603,9 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box.row().operator(KITSU_OT_build_config_save_settings.bl_idname, icon="TEXT") box.row().operator(KITSU_OT_build_config_save_templates.bl_idname, icon="FILE_BLEND") + # Render Review + self.draw_render_review(col) + # Misc settings. box = col.box() box.label(text="Miscellaneous", icon="MODIFIER") @@ -582,6 +619,27 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box.row().prop(self, "shot_counter_digits") box.row().prop(self, "shot_counter_increment") + def draw_render_review(self, layout: bpy.types.UILayout) -> None: + box = layout.box() + box.label(text="Render Review", icon="FILEBROWSER") + + # Farm outpur dir. + box.row().prop(self, "farm_output_dir") + + if not self.farm_output_dir: + row = box.row() + row.label(text="Please specify the Farm Output Directory", icon="ERROR") + + if not bpy.data.filepath and self.farm_output_dir.startswith("//"): + row = box.row() + row.label( + text="In order to use a relative path the current file needs to be saved.", + icon="ERROR", + ) + + box.row().prop(self, "shot_name_filter") + box.row().prop(self, "versions_max_count", slider=True) + @property def shot_playblast_root_path(self) -> Optional[Path]: if not self.is_playblast_root_valid: @@ -646,6 +704,24 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): return True + @property + def farm_output_path(self) -> Optional[Path]: + if not self.is_farm_output_valid: + return None + return Path(os.path.abspath(bpy.path.abspath(self.farm_output_dir))) + + @property + def is_farm_output_valid(self) -> bool: + + # Check if file is saved. + if not self.farm_output_dir: + return False + + if not bpy.data.filepath and self.farm_output_dir.startswith("//"): + return False + + return True + def session_get(context: bpy.types.Context) -> Session: """ diff --git a/scripts-blender/addons/blender_kitsu/render_review/README.md b/scripts-blender/addons/blender_kitsu/render_review/README.md new file mode 100644 index 00000000..e69de29b diff --git a/scripts-blender/addons/render_review/__init__.py b/scripts-blender/addons/blender_kitsu/render_review/__init__.py similarity index 70% rename from scripts-blender/addons/render_review/__init__.py rename to scripts-blender/addons/blender_kitsu/render_review/__init__.py index 724c80ae..9fc6cbe6 100644 --- a/scripts-blender/addons/render_review/__init__.py +++ b/scripts-blender/addons/blender_kitsu/render_review/__init__.py @@ -20,7 +20,7 @@ import bpy -from render_review import ( +from . import ( util, props, kitsu, @@ -28,25 +28,9 @@ from render_review import ( checksqe, ops, ui, - prefs, draw, ) -from render_review.log import LoggerFactory -logger = LoggerFactory.getLogger(__name__) - -bl_info = { - "name": "Render Review", - "author": "Paul Golter", - "description": "Addon to review renders from Flamenco with the Sequence Editor", - "blender": (3, 0, 0), - "version": (0, 1, 4), - "location": "Sequence Editor", - "warning": "", - "doc_url": "", - "tracker_url": "", - "category": "Generic", -} _need_reload = "ops" in locals() @@ -56,7 +40,6 @@ if _need_reload: util = importlib.reload(util) props = importlib.reload(props) - prefs = importlib.reload(prefs) kitsu = importlib.reload(kitsu) opsdata = importlib.reload(opsdata) checksqe = importlib.reload(checksqe) @@ -64,22 +47,16 @@ if _need_reload: ui = importlib.reload(ui) draw = importlib.reload(draw) + def register(): props.register() - prefs.register() ops.register() ui.register() draw.register() - logger.info("Registered render-review") def unregister(): draw.unregister() ui.unregister() ops.unregister() - prefs.unregister() props.unregister() - - -if __name__ == "__main__": - register() diff --git a/scripts-blender/addons/render_review/checksqe.py b/scripts-blender/addons/blender_kitsu/render_review/checksqe.py similarity index 100% rename from scripts-blender/addons/render_review/checksqe.py rename to scripts-blender/addons/blender_kitsu/render_review/checksqe.py diff --git a/scripts-blender/addons/render_review/draw.py b/scripts-blender/addons/blender_kitsu/render_review/draw.py similarity index 100% rename from scripts-blender/addons/render_review/draw.py rename to scripts-blender/addons/blender_kitsu/render_review/draw.py diff --git a/scripts-blender/addons/render_review/exception.py b/scripts-blender/addons/blender_kitsu/render_review/exception.py similarity index 100% rename from scripts-blender/addons/render_review/exception.py rename to scripts-blender/addons/blender_kitsu/render_review/exception.py diff --git a/scripts-blender/addons/render_review/kitsu.py b/scripts-blender/addons/blender_kitsu/render_review/kitsu.py similarity index 92% rename from scripts-blender/addons/render_review/kitsu.py rename to scripts-blender/addons/blender_kitsu/render_review/kitsu.py index 20fd86c6..7feb00dc 100644 --- a/scripts-blender/addons/render_review/kitsu.py +++ b/scripts-blender/addons/blender_kitsu/render_review/kitsu.py @@ -18,16 +18,17 @@ # # (c) 2021, Blender Foundation - Paul Golter +# TODO Remove duplicate code between this page and the main Blender Kitsu functions from typing import List, Dict, Union, Any, Set, Optional import bpy -from render_review import util -from blender_kitsu import cache, types, prefs -from blender_kitsu.sqe import opsdata, pull, push -from render_review.log import LoggerFactory +from . import util +from .. import cache, types, prefs +from ..sqe import opsdata, pull, push +from ..logger import LoggerFactory -logger = LoggerFactory.getLogger(name=__name__) +logger = LoggerFactory.getLogger() def is_auth() -> bool: diff --git a/scripts-blender/addons/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py similarity index 94% rename from scripts-blender/addons/render_review/ops.py rename to scripts-blender/addons/blender_kitsu/render_review/ops.py index c9e3a0a3..deeb74cc 100644 --- a/scripts-blender/addons/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -28,16 +28,15 @@ from collections import OrderedDict import bpy -from render_review import vars, prefs, opsdata, util, kitsu +from . import vars, opsdata, util, kitsu +from .. import prefs, cache +from .. import types as kitsu_types -if prefs.is_blender_kitsu_enabled(): - from blender_kitsu import types as kitsu_types - from blender_kitsu import cache +from ..logger import LoggerFactory -from render_review.exception import NoImageSequenceAvailableException -from render_review.log import LoggerFactory +from .exception import NoImageSequenceAvailableException -logger = LoggerFactory.getLogger(name=__name__) +logger = LoggerFactory.getLogger() class RR_OT_sqe_create_review_session(bpy.types.Operator): @@ -69,9 +68,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): or opsdata.is_sequence_dir(render_dir) ) - def load_strip_from_img_seq( - self, context, directory, idx: int, frame_start: int = 0 - ): + def load_strip_from_img_seq(self, context, directory, idx: int, frame_start: int = 0): try: # Get best preview files sequence. image_sequence = opsdata.get_best_preview_sequence(directory) @@ -92,9 +89,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): logger.info("%s found %i exr frames", directory.name, len(image_sequence)) else: - logger.info( - "%s found %i preview frames", directory.name, len(image_sequence) - ) + logger.info("%s found %i preview frames", directory.name, len(image_sequence)) finally: # Get frame start. @@ -269,8 +264,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): logger.info("Processing %s", shot_folder.name) use_video = addon_prefs.use_video and not ( - shot_folder != shot_version_folders[-1] - and addon_prefs.use_video_latest_only + shot_folder != shot_version_folders[-1] and addon_prefs.use_video_latest_only ) shot_strip = self.import_shot_as_strip( @@ -317,9 +311,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): fit_method='ORIGINAL', ) else: - strip = self.load_strip_from_img_seq( - context, shot_folder, channel_idx, frame_start - ) + strip = self.load_strip_from_img_seq(context, shot_folder, channel_idx, frame_start) shot_datetime = datetime.fromtimestamp(shot_folder.stat().st_mtime) time_str = shot_datetime.strftime("%B %d, %I:%M") @@ -351,7 +343,7 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): sequence: bpy.props.EnumProperty( name="Sequence", description="Select which sequence to review", - items=sequences_enum_items if prefs.is_blender_kitsu_enabled() else [], + items=sequences_enum_items, ) @staticmethod @@ -401,7 +393,7 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): row.prop(addon_prefs, 'use_video') if addon_prefs.use_video: row.prop(addon_prefs, 'use_video_latest_only') - + layout.prop(addon_prefs, 'shot_name_filter') def execute(self, context: bpy.types.Context) -> Set[str]: @@ -440,9 +432,7 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): bl_idname = "rr.sqe_inspect_exr_sequence" bl_label = "Inspect EXR" - bl_description = ( - "Loads EXR sequence for selected sequence strip in image editor, if it exists" - ) + bl_description = "Loads EXR sequence for selected sequence strip in image editor, if it exists" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -465,9 +455,7 @@ class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): output_dir = opsdata.get_strip_folder(active_strip) # Find exr sequence. - exr_seq = [ - f for f in output_dir.iterdir() if f.is_file() and f.suffix == ".exr" - ] + exr_seq = [f for f in output_dir.iterdir() if f.is_file() and f.suffix == ".exr"] exr_seq.sort(key=lambda p: p.name) exr_seq_frame_start = int(exr_seq[0].stem) @@ -583,9 +571,7 @@ class RR_OT_sqe_approve_render(bpy.types.Operator): shot_frames_dir, dirs_exist_ok=True, ) - logger.info( - "Copied: %s \nTo: %s", strip_dir.as_posix(), shot_frames_dir.as_posix() - ) + logger.info("Copied: %s \nTo: %s", strip_dir.as_posix(), shot_frames_dir.as_posix()) # Update metadata json. if not metadata_path.exists(): @@ -702,9 +688,7 @@ class RR_OT_open_path(bpy.types.Operator): os.startfile(filepath.as_posix()) else: - self.report( - {"ERROR"}, f"Can't open explorer. Unsupported platform {sys.platform}" - ) + self.report({"ERROR"}, f"Can't open explorer. Unsupported platform {sys.platform}") return {"CANCELLED"} return {"FINISHED"} @@ -794,9 +778,7 @@ class RR_OT_sqe_push_to_edit(bpy.types.Operator): active_strip = context.scene.sequence_editor.active_strip return bool( - active_strip - and active_strip.rr.is_render - and not active_strip.rr.is_pushed_to_edit + active_strip and active_strip.rr.is_render and not active_strip.rr.is_pushed_to_edit ) def execute(self, context: bpy.types.Context) -> Set[str]: @@ -812,9 +794,7 @@ class RR_OT_sqe_push_to_edit(bpy.types.Operator): mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) except NoImageSequenceAvailableException: # No jpeg files available. - self.report( - {"ERROR"}, f"No preview files available in {render_dir.as_posix()}" - ) + self.report({"ERROR"}, f"No preview files available in {render_dir.as_posix()}") return {"CANCELLED"} # If mp4 path does not exist, use ffmpeg to create preview file. @@ -832,18 +812,14 @@ class RR_OT_sqe_push_to_edit(bpy.types.Operator): # Create edit path if not exists yet. if not shot_previews_dir.exists(): shot_previews_dir.mkdir(parents=True) - logger.info( - "Created dir in Shot Previews: %s", shot_previews_dir.as_posix() - ) + logger.info("Created dir in Shot Previews: %s", shot_previews_dir.as_posix()) # Get edit_filepath. edit_filepath = self.get_edit_filepath(active_strip) # Copy mp4 to edit filepath. shutil.copy2(mp4_path.as_posix(), edit_filepath.as_posix()) - logger.info( - "Copied: %s \nTo: %s", mp4_path.as_posix(), edit_filepath.as_posix() - ) + logger.info("Copied: %s \nTo: %s", mp4_path.as_posix(), edit_filepath.as_posix()) # ----------------UPDATE METADATA.JSON ------------------. @@ -881,9 +857,7 @@ class RR_OT_sqe_push_to_edit(bpy.types.Operator): mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) except NoImageSequenceAvailableException: layout.separator() - layout.row(align=True).label( - text="No preview files available", icon="ERROR" - ) + layout.row(align=True).label(text="No preview files available", icon="ERROR") return text = "From Farm Output:" @@ -983,9 +957,7 @@ def register(): # Isolate strip. addon_keymap_items.append( - keymap.keymap_items.new( - "rr.sqe_isolate_strip_enter", value="PRESS", type="ONE" - ) + keymap.keymap_items.new("rr.sqe_isolate_strip_enter", value="PRESS", type="ONE") ) # Umute all. @@ -995,9 +967,7 @@ def register(): ) ) for kmi in addon_keymap_items: - logger.info( - "Registered new hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name - ) + logger.info("Registered new hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name) def unregister(): diff --git a/scripts-blender/addons/render_review/opsdata.py b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py similarity index 87% rename from scripts-blender/addons/render_review/opsdata.py rename to scripts-blender/addons/blender_kitsu/render_review/opsdata.py index 308ecc22..a1cbfc53 100644 --- a/scripts-blender/addons/render_review/opsdata.py +++ b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py @@ -25,11 +25,14 @@ from typing import Set, Union, Optional, List, Dict, Any, Tuple import bpy -from render_review import vars, prefs, checksqe, prefs -from render_review.log import LoggerFactory -from render_review.exception import NoImageSequenceAvailableException +from . import vars, checksqe +from .. import prefs +from .exception import NoImageSequenceAvailableException + +from ..logger import LoggerFactory + +logger = LoggerFactory.getLogger() -logger = LoggerFactory.getLogger(name=__name__) copytree_list: List[Path] = [] copytree_num_of_items: int = 0 @@ -78,9 +81,7 @@ def get_valid_cs_sequences( if sequence_list: sequences = sequence_list else: - sequences = ( - context.selected_sequences or context.scene.sequence_editor.sequences_all - ) + sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all if prefs.is_blender_kitsu_enabled(): @@ -90,9 +91,7 @@ def get_valid_cs_sequences( if s.type in ["MOVIE", "IMAGE"] and not s.mute and not s.kitsu.initialized ] else: - valid_sequences = [ - s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute - ] + valid_sequences = [s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute] return valid_sequences @@ -101,10 +100,7 @@ def get_shot_frames_dir(strip: bpy.types.Sequence) -> Path: # sf = shot_frames | fo = farm_output. addon_prefs = prefs.addon_prefs_get(bpy.context) fo_dir = get_strip_folder(strip) - sf_dir = ( - addon_prefs.shot_frames_dir - / fo_dir.parent.relative_to(fo_dir.parents[3]) - ) + sf_dir = addon_prefs.shot_frames_dir / fo_dir.parent.relative_to(fo_dir.parents[3]) return sf_dir @@ -120,9 +116,8 @@ def get_shot_previews_path(strip: bpy.types.Sequence) -> Path: # Fo > farm_output. addon_prefs = prefs.addon_prefs_get(bpy.context) fo_dir = get_strip_folder(strip) - shot_previews_dir = ( - addon_prefs.shot_previews_path - / fo_dir.parent.relative_to(fo_dir.parents[3]) + shot_previews_dir = addon_prefs.shot_previews_path / fo_dir.parent.relative_to( + fo_dir.parents[3] ) return shot_previews_dir @@ -157,9 +152,7 @@ def get_best_preview_sequence(dir: Path) -> List[Path]: dir, output=dict, search_suffixes=[".jpg", ".png"] ) if not files: - raise NoImageSequenceAvailableException( - f"No preview files found in: {dir.as_posix()}" - ) + raise NoImageSequenceAvailableException(f"No preview files found in: {dir.as_posix()}") # Select the right images sequence. if len(files) == 1: @@ -187,6 +180,7 @@ def get_shot_frames_metadata_path(strip: bpy.types.Sequence) -> Path: fs_dir = get_shot_frames_dir(strip) return fs_dir.parent / "metadata.json" + def get_shot_previews_metadata_path(strip: bpy.types.Sequence) -> Path: fs_dir = get_shot_previews_path(strip) return fs_dir / "metadata.json" @@ -208,14 +202,11 @@ def update_sequence_statuses( ) -> List[bpy.types.Sequence]: return update_is_approved(context), update_is_pushed_to_edit(context) + def update_is_approved( context: bpy.types.Context, ) -> List[bpy.types.Sequence]: - sequences = [ - s - for s in context.scene.sequence_editor.sequences_all - if s.rr.is_render - ] + sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] approved_strips = [] @@ -223,9 +214,7 @@ def update_is_approved( metadata_path = get_shot_frames_metadata_path(s) if not metadata_path.exists(): continue - json_obj = load_json( - metadata_path - ) # TODO: prevent opening same json multi times + json_obj = load_json(metadata_path) # TODO: prevent opening same json multi times if Path(json_obj["source_current"]) == get_strip_folder(s): s.rr.is_approved = True @@ -240,11 +229,7 @@ def update_is_approved( def update_is_pushed_to_edit( context: bpy.types.Context, ) -> List[bpy.types.Sequence]: - sequences = [ - s - for s in context.scene.sequence_editor.sequences_all - if s.rr.is_render - ] + sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] pushed_strips = [] @@ -253,9 +238,7 @@ def update_is_pushed_to_edit( if not metadata_path.exists(): continue - json_obj = load_json( - metadata_path - ) + json_obj = load_json(metadata_path) valid_paths = {Path(value).parent for _key, value in json_obj.items()} @@ -319,12 +302,8 @@ def gather_files_by_suffix( ) -def gen_frames_found_text( - dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"] -) -> str: - files_dict = gather_files_by_suffix( - dir, output=dict, search_suffixes=search_suffixes - ) +def gen_frames_found_text(dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"]) -> str: + files_dict = gather_files_by_suffix(dir, output=dict, search_suffixes=search_suffixes) frames_found_text = "" # frames found text will be used in ui for suffix, file_list in files_dict.items(): @@ -387,7 +366,7 @@ def fit_frame_range_to_strips( strips.sort(key=get_sort_tuple) context.scene.frame_start = strips[0].frame_final_start - context.scene.frame_end = strips[-1].frame_final_end -1 + context.scene.frame_end = strips[-1].frame_final_end - 1 return (context.scene.frame_start, context.scene.frame_end) @@ -413,6 +392,7 @@ def get_top_level_valid_strips_continious( return sequences + def setup_color_management(context: bpy.types.Context) -> None: if context.scene.view_settings.view_transform != 'Standard': context.scene.view_settings.view_transform = 'Standard' diff --git a/scripts-blender/addons/render_review/props.py b/scripts-blender/addons/blender_kitsu/render_review/props.py similarity index 96% rename from scripts-blender/addons/render_review/props.py rename to scripts-blender/addons/blender_kitsu/render_review/props.py index e3702646..a6be5b28 100644 --- a/scripts-blender/addons/render_review/props.py +++ b/scripts-blender/addons/blender_kitsu/render_review/props.py @@ -23,9 +23,10 @@ from pathlib import Path import bpy -from render_review.log import LoggerFactory -logger = LoggerFactory.getLogger(name=__name__) +from ..logger import LoggerFactory + +logger = LoggerFactory.getLogger() class RR_isolate_collection_prop(bpy.types.PropertyGroup): diff --git a/scripts-blender/addons/render_review/ui.py b/scripts-blender/addons/blender_kitsu/render_review/ui.py similarity index 88% rename from scripts-blender/addons/render_review/ui.py rename to scripts-blender/addons/blender_kitsu/render_review/ui.py index 3f213488..c6d82772 100644 --- a/scripts-blender/addons/render_review/ui.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ui.py @@ -24,7 +24,7 @@ from typing import Set, Union, Optional, List, Dict, Any import bpy -from render_review.ops import ( +from .ops import ( RR_OT_sqe_create_review_session, RR_OT_setup_review_workspace, RR_OT_sqe_inspect_exr_sequence, @@ -34,7 +34,8 @@ from render_review.ops import ( RR_OT_open_path, RR_OT_sqe_push_to_edit, ) -from render_review import opsdata, prefs, kitsu +from . import opsdata, kitsu +from .. import prefs class RR_PT_render_review(bpy.types.Panel): @@ -98,9 +99,7 @@ class RR_PT_render_review(bpy.types.Panel): # Create box. layout = self.layout box = layout.box() - box.label( - text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF" - ) + box.label(text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF") box.separator() # Render dir name label and open file op. @@ -112,9 +111,7 @@ class RR_PT_render_review(bpy.types.Panel): ).filepath = bpy.path.abspath(directory.as_posix()) # Nr of frames. - box.row(align=True).label( - text=f"Frames: {active_strip.rr.frames_found_text}" - ) + box.row(align=True).label(text=f"Frames: {active_strip.rr.frames_found_text}") # Inspect exr. text = "Inspect EXR" @@ -134,24 +131,19 @@ class RR_PT_render_review(bpy.types.Panel): if active_strip.rr.is_pushed_to_edit: text = "Approve Render" row.operator(RR_OT_sqe_approve_render.bl_idname, icon="CHECKMARK", text=text) - row.operator( - RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH" - ) + row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH") # Push to edit. if not addon_prefs.shot_previews_path: shot_previews_dir = "" # ops handle invalid path else: - shot_previews_dir = Path( - opsdata.get_shot_previews_path(active_strip) - ).as_posix() + shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix() row = box.row(align=True) row.operator(RR_OT_sqe_push_to_edit.bl_idname, icon="EXPORT") - row.operator( - RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="" - ).filepath = shot_previews_dir - + row.operator(RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="").filepath = ( + shot_previews_dir + ) if prefs.is_blender_kitsu_enabled(): # Push strip to Kitsu. diff --git a/scripts-blender/addons/render_review/util.py b/scripts-blender/addons/blender_kitsu/render_review/util.py similarity index 97% rename from scripts-blender/addons/render_review/util.py rename to scripts-blender/addons/blender_kitsu/render_review/util.py index 1516db43..1da430ef 100644 --- a/scripts-blender/addons/render_review/util.py +++ b/scripts-blender/addons/blender_kitsu/render_review/util.py @@ -21,7 +21,7 @@ import re from typing import Union, Dict, List, Any import bpy -from render_review import vars +from . import vars def redraw_ui() -> None: diff --git a/scripts-blender/addons/render_review/vars.py b/scripts-blender/addons/blender_kitsu/render_review/vars.py similarity index 100% rename from scripts-blender/addons/render_review/vars.py rename to scripts-blender/addons/blender_kitsu/render_review/vars.py diff --git a/scripts-blender/addons/render_review/.gitignore b/scripts-blender/addons/render_review/.gitignore deleted file mode 100644 index d197a693..00000000 --- a/scripts-blender/addons/render_review/.gitignore +++ /dev/null @@ -1,112 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -.venv* -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -# IDE settings -.vscode/ - -# utility bat files: -*jump_in_venv.bat - -#local tests -tests/local* \ No newline at end of file diff --git a/scripts-blender/addons/render_review/CHANGELOG.md b/scripts-blender/addons/render_review/CHANGELOG.md deleted file mode 100644 index 7e4dad70..00000000 --- a/scripts-blender/addons/render_review/CHANGELOG.md +++ /dev/null @@ -1,34 +0,0 @@ -## 0.1.4 - 2024-02-23 - -### FIXED -- Fix Render Review Metastrips (use new naming convention) -- Fix File Delimiter (#224) -- Fix Bug loading Kitsu Project (#223) -- Fix EXR Colorspace Name - -## 0.1.3 - 2023-08-02 - -### FIXED -- Fix Changelog Rendering (#125) -- Fix Import Errors (#122) -- Fix Changelogs -- Fix line ends from DOS to UNIX (#68) - -### REMOVED -- Remove Metastrip Filepath (#80) - -## 0.1.2 - 2023-06-19 - -### FIXED -- Fix line ends from DOS to UNIX (#68) - -### REMOVED -- Remove Metastrip Filepath (#80) - - -## 0.1.1 - 2023-06-02 - -### CHANGED -- Always use Video Editing workspace (#61) - - diff --git a/scripts-blender/addons/render_review/LICENSE b/scripts-blender/addons/render_review/LICENSE deleted file mode 100644 index 328b70f8..00000000 --- a/scripts-blender/addons/render_review/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - PlaySync - Copyright (C) 2020 Blender - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - PlaySync Copyright (C) 2020 Blender - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/scripts-blender/addons/render_review/README.md b/scripts-blender/addons/render_review/README.md deleted file mode 100644 index fc1c89fd..00000000 --- a/scripts-blender/addons/render_review/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Render Review -Blender Add-on to review renders from Flamenco with the Sequence Editor - -## Table of Contents -- [Installation](#installation) -- [Before you get started](#before-you-get-started) -- [Features](#features) - -## Installation -1. Download [latest release](../addons/overview) -2. Launch Blender, navigate to `Edit > Preferences` select `Addons` and then `Install`, -3. Navigate to the downloaded add-on and select `Install Add-on` - -After install you need to configure the addon in the addon preferences. - -## Before you get started - -This addon requires a specific folder structure of the rendering pipeline. This structure is defined by Flamenco - -If you have a different folder structure the addon might not work as -expected. - -## Features -- Quickly load all versions of a shot or a whole sequence that was rendered with Flamenco in to the Sequence Editor -- Inspect EXR's of selected sequence strip with one click -- Approve render which copies data from the farm_output to the shot_frames folder -- Push a render to the edit which uses the existing .mp4 preview or creates it with ffmpeg -and copies it over to the shot_preview folder with automatic versioning incrementation -- Creation of metadata.json files on approving renders and pushing renders to edit to keep track where a file came from -- Connection to `blender-kitsu` addon, that can be enabled and extends the functionality of some operators - -## Links - -Flamenco Doc diff --git a/scripts-blender/addons/render_review/log.py b/scripts-blender/addons/render_review/log.py deleted file mode 100644 index 08401346..00000000 --- a/scripts-blender/addons/render_review/log.py +++ /dev/null @@ -1,33 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import logging - - -class LoggerFactory: - - """ - Utility class to streamline logger creation - """ - - @staticmethod - def getLogger(name=__name__): - logger = logging.getLogger(name) - return logger diff --git a/scripts-blender/addons/render_review/prefs.py b/scripts-blender/addons/render_review/prefs.py deleted file mode 100644 index 89649b57..00000000 --- a/scripts-blender/addons/render_review/prefs.py +++ /dev/null @@ -1,259 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import os -import bpy -from pathlib import Path -from typing import Optional, Dict, List, Set, Any - -import bpy - - -def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences: - """ - shortcut to get addon preferences - """ - return context.preferences.addons["render_review"].preferences - - -def is_blender_kitsu_enabled() -> bool: - return __package__ in bpy.context.preferences.addons - - -class RR_OT_enable_blender_kitsu(bpy.types.Operator): - bl_idname = "rr.enable_blender_kitsu" - bl_label = "Enable Blender Kitsu" - bl_description = ( - "Enables connection to blender_kitsu which " - "which adds additional functionality to operators " - "Blender-kitsu needs to be installed and enabled" - ) - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context: bpy.types.Context) -> Set[str]: - addon_prefs = addon_prefs_get(context) - - # Enable_blender_kitsu checkbox is off -> user wants to enable it. - if not addon_prefs.enable_blender_kitsu: - if not is_blender_kitsu_enabled(): - self.report({"ERROR"}, "blender_kitsu is not enabled or installed") - return {"CANCELLED"} - - addon_prefs.enable_blender_kitsu = True - return {"FINISHED"} - - # Disable blender_kitsu, checkbox is on. - else: - addon_prefs.enable_blender_kitsu = False - return {"FINISHED"} - - -class RR_AddonPreferences(bpy.types.AddonPreferences): - bl_idname = __package__ - - def _check_blender_kitsu_installed(self, value): - if not is_blender_kitsu_enabled(): - raise RuntimeError("blender_kitsu addon ist not enabled") - - farm_output_dir: bpy.props.StringProperty( # type: ignore - name="Farm Output Directory", - description="Should point to: /render/sprites/farm_output", - default="/render/sprites/farm_output", - subtype="DIR_PATH", - ) - - shot_frames_dir: bpy.props.StringProperty( # type: ignore - name="Shot Frames Directory", - description="Should point to: /render/sprites/shot_frames", - default="/render/sprites/shot_frames", - subtype="DIR_PATH", - ) - - shot_previews_dir: bpy.props.StringProperty( # type: ignore - name="Shot Previews Directory ", - description="Should point to: /render/sprites/shot_previews", - default="", - subtype="DIR_PATH", - ) - - shot_name_filter: bpy.props.StringProperty( # type: ignore - name="Shot Name Filter", - description="Shot name must include this string, otherwise it will be ignored", - default="", - ) - - enable_blender_kitsu: bpy.props.BoolProperty( - name="Enable Blender Kitsu", - description="This checkbox controls if render_review should try to use the blender_kitsu addon to extend its feature sets", - # Set=_check_blender_kitsu_installed,. - default=False, - ) - - use_video: bpy.props.BoolProperty( - name="Use Video", - description="Load video versions of renders rather than image sequences for faster playback" - ) - use_video_latest_only: bpy.props.BoolProperty( - default=True, - name="Latest Only", - description="Only load video files for the latest versions by default, to avoid running out of memory and crashing" - ) - - versions_max_count: bpy.props.IntProperty( - name = "Max Versions", - description = "Desired number of versions to load for each shot", - min = 1, - max = 128, - default = 32, - ) - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - box = layout.box() - box.label(text="Filepaths", icon="FILEBROWSER") - - # Farm outpur dir. - box.row().prop(self, "farm_output_dir") - - if not self.farm_output_dir: - row = box.row() - row.label(text="Please specify the Farm Output Directory", icon="ERROR") - - if not bpy.data.filepath and self.farm_output_dir.startswith("//"): - row = box.row() - row.label( - text="In order to use a relative path the current file needs to be saved.", - icon="ERROR", - ) - - # Shot Frames dir. - box.row().prop(self, "shot_frames_dir") - - if not self.shot_frames_dir: - row = box.row() - row.label(text="Please specify the Shot Frames Directory", icon="ERROR") - - if not bpy.data.filepath and self.shot_frames_dir.startswith("//"): - row = box.row() - row.label( - text="In order to use a relative path the current file needs to be saved.", - icon="ERROR", - ) - - # Shot Previews dir. - box.row().prop(self, "shot_previews_dir") - - if not self.shot_previews_dir: - row = box.row() - row.label(text="Please specify the Shots Preview Directory", icon="ERROR") - - if not bpy.data.filepath and self.shot_previews_dir.startswith("//"): - row = box.row() - row.label( - text="In order to use a relative path the current file needs to be saved.", - icon="ERROR", - ) - - box.separator() - box.row().prop(self, "shot_name_filter") - box.row().prop(self, "versions_max_count", slider=True) - - # Enable blender kitsu. - icon = "CHECKBOX_DEHLT" - label_text = "Enable Blender Kitsu" - - if self.enable_blender_kitsu: - icon = "CHECKBOX_HLT" - - row = box.row(align=True) - row.operator( - RR_OT_enable_blender_kitsu.bl_idname, icon=icon, text="", emboss=False - ) - row.label(text=label_text) - - - @property - def shot_frames_dir(self) -> Optional[Path]: - if not self.is_shot_frames_valid: - return None - return Path(os.path.abspath(bpy.path.abspath(self.shot_frames_dir))) - - @property - def is_shot_frames_valid(self) -> bool: - - # Check if file is saved. - if not self.shot_frames_dir: - return False - - if not bpy.data.filepath and self.shot_frames_dir.startswith("//"): - return False - - return True - - @property - def farm_output_path(self) -> Optional[Path]: - if not self.is_farm_output_valid: - return None - return Path(os.path.abspath(bpy.path.abspath(self.farm_output_dir))) - - @property - def is_farm_output_valid(self) -> bool: - - # Check if file is saved. - if not self.farm_output_dir: - return False - - if not bpy.data.filepath and self.farm_output_dir.startswith("//"): - return False - - return True - - @property - def shot_previews_path(self) -> Optional[Path]: - if not self.is_shot_previews_valid: - return None - return Path(os.path.abspath(bpy.path.abspath(self.shot_previews_dir))) - - @property - def is_shot_previews_valid(self) -> bool: - - # Check if file is saved. - if not self.shot_previews_dir: - return False - - if not bpy.data.filepath and self.shot_previews_dir.startswith("//"): - return False - - return True - - -# ---------REGISTER ----------. - -classes = [RR_OT_enable_blender_kitsu, RR_AddonPreferences] - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) -- 2.30.2 From 24f22fd89aefa9c5e42d9d4ddfcd3e96bb6d8859 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:43 -0400 Subject: [PATCH 02/17] Use Blender Kitsu Playblast / Frames Directories --- .../blender_kitsu/render_review/__init__.py | 4 +- .../render_review/{kitsu.py => core.py} | 19 +------ .../addons/blender_kitsu/render_review/ops.py | 53 ++++++++----------- .../blender_kitsu/render_review/opsdata.py | 14 ++--- .../addons/blender_kitsu/render_review/ui.py | 26 +++++---- 5 files changed, 45 insertions(+), 71 deletions(-) rename scripts-blender/addons/blender_kitsu/render_review/{kitsu.py => core.py} (87%) diff --git a/scripts-blender/addons/blender_kitsu/render_review/__init__.py b/scripts-blender/addons/blender_kitsu/render_review/__init__.py index 9fc6cbe6..4741dacb 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/__init__.py +++ b/scripts-blender/addons/blender_kitsu/render_review/__init__.py @@ -23,7 +23,7 @@ import bpy from . import ( util, props, - kitsu, + core, opsdata, checksqe, ops, @@ -40,7 +40,7 @@ if _need_reload: util = importlib.reload(util) props = importlib.reload(props) - kitsu = importlib.reload(kitsu) + core = importlib.reload(core) opsdata = importlib.reload(opsdata) checksqe = importlib.reload(checksqe) ops = importlib.reload(ops) diff --git a/scripts-blender/addons/blender_kitsu/render_review/kitsu.py b/scripts-blender/addons/blender_kitsu/render_review/core.py similarity index 87% rename from scripts-blender/addons/blender_kitsu/render_review/kitsu.py rename to scripts-blender/addons/blender_kitsu/render_review/core.py index 7feb00dc..0b6f66cc 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/kitsu.py +++ b/scripts-blender/addons/blender_kitsu/render_review/core.py @@ -18,7 +18,6 @@ # # (c) 2021, Blender Foundation - Paul Golter -# TODO Remove duplicate code between this page and the main Blender Kitsu functions from typing import List, Dict, Union, Any, Set, Optional import bpy @@ -31,26 +30,11 @@ from ..logger import LoggerFactory logger = LoggerFactory.getLogger() -def is_auth() -> bool: - return prefs.addon_prefs_get(bpy.context).session.is_auth() - - -def get_project() -> Optional[types.Project]: - return cache.project_active_get() - - def is_active_project() -> bool: return bool(cache.project_active_get()) -def is_auth_and_project() -> bool: - return bool(is_auth() and is_active_project()) - - -def addon_prefs() -> bpy.types.AddonPreferences: - return prefs.addon_prefs_get(bpy.context) - - +# TODO De-duplicate code from sqe create metastrip def create_metadata_strip( context: bpy.types.Context, strip: bpy.types.Sequence ) -> bpy.types.MovieSequence: @@ -87,6 +71,7 @@ def create_metadata_strip( return metadata_strip +# TODO De-duplicate code from sqe code def link_strip_by_name( context: bpy.types.Context, strip: bpy.types.Sequence, diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index deeb74cc..3630d1d3 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -28,7 +28,7 @@ from collections import OrderedDict import bpy -from . import vars, opsdata, util, kitsu +from . import vars, opsdata, util, core from .. import prefs, cache from .. import types as kitsu_types @@ -145,19 +145,15 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): prev_frame_end = strip_longest.frame_final_end # Perform kitsu operations if enabled. - if ( - addon_prefs.enable_blender_kitsu - and prefs.is_blender_kitsu_enabled() - and imported_strips - ): - if kitsu.is_auth_and_project(): + if prefs.session_auth(context) and imported_strips: + if core.is_active_project(): sequence_name = shot_version_folders[0].parent.parent.parent.name # Create metadata strip. - metadata_strip = kitsu.create_metadata_strip(context, strip_longest) + metadata_strip = core.create_metadata_strip(context, strip_longest) # Link metadata strip. - kitsu.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) + core.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) else: logger.error( @@ -167,13 +163,11 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): # Set default scene resolution to resolution of loaded image. render_resolution_x = vars.RESOLUTION[0] render_resolution_y = vars.RESOLUTION[1] + project = cache.project_active_get() # If Kitsu add-on is enabled, fetch the resolution from the online project - if ( - addon_prefs.enable_blender_kitsu and prefs.is_blender_kitsu_enabled() - ) and kitsu.is_active_project(): + if project: # TODO: make the resolution fetching a bit more robust # Assume resolution is a string 'x' - project = kitsu.get_project() resolution = project.resolution.split('x') render_resolution_x = int(resolution[0]) render_resolution_y = int(resolution[1]) @@ -371,11 +365,8 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): area.spaces.active.show_overlays = False def invoke(self, context, _event): - if ( - not prefs.is_blender_kitsu_enabled() - or not prefs.addon_prefs_get(context).enable_blender_kitsu - or not kitsu.is_auth_and_project() - ): + + if not cache.project_active_get(): return self.execute(context) return context.window_manager.invoke_props_dialog(self) @@ -544,34 +535,34 @@ class RR_OT_sqe_approve_render(bpy.types.Operator): bpy.ops.rr.sqe_push_to_edit() strip_dir = opsdata.get_strip_folder(active_strip) - shot_frames_dir = opsdata.get_shot_frames_dir(active_strip) + frames_root_dir = opsdata.get_frames_root_dir(active_strip) shot_frames_backup_path = opsdata.get_shot_frames_backup_path(active_strip) metadata_path = opsdata.get_shot_frames_metadata_path(active_strip) # Create Shot Frames path if not exists yet. - if shot_frames_dir.exists(): + if frames_root_dir.exists(): # Delete backup if exists. if shot_frames_backup_path.exists(): shutil.rmtree(shot_frames_backup_path) # Rename current to backup. - shot_frames_dir.rename(shot_frames_backup_path) + frames_root_dir.rename(shot_frames_backup_path) logger.info( "Created backup: %s > %s", - shot_frames_dir.name, + frames_root_dir.name, shot_frames_backup_path.name, ) else: - shot_frames_dir.mkdir(parents=True) - logger.info("Created dir in Shot Frames: %s", shot_frames_dir.as_posix()) + frames_root_dir.mkdir(parents=True) + logger.info("Created dir in Shot Frames: %s", frames_root_dir.as_posix()) # Copy dir. opsdata.copytree_verbose( strip_dir, - shot_frames_dir, + frames_root_dir, dirs_exist_ok=True, ) - logger.info("Copied: %s \nTo: %s", strip_dir.as_posix(), shot_frames_dir.as_posix()) + logger.info("Copied: %s \nTo: %s", strip_dir.as_posix(), frames_root_dir.as_posix()) # Update metadata json. if not metadata_path.exists(): @@ -595,22 +586,22 @@ class RR_OT_sqe_approve_render(bpy.types.Operator): util.redraw_ui() # Log. - self.report({"INFO"}, f"Updated {shot_frames_dir.name} in Shot Frames") + self.report({"INFO"}, f"Updated {frames_root_dir.name} in Shot Frames") logger.info("Updated metadata in: %s", metadata_path.as_posix()) return {"FINISHED"} def invoke(self, context, event): active_strip = context.scene.sequence_editor.active_strip - shot_frames_dir = opsdata.get_shot_frames_dir(active_strip) - width = 200 + len(shot_frames_dir.as_posix()) * 5 + frames_root_dir = opsdata.get_frames_root_dir(active_strip) + width = 200 + len(frames_root_dir.as_posix()) * 5 return context.window_manager.invoke_props_dialog(self, width=width) def draw(self, context: bpy.types.Context) -> None: layout = self.layout active_strip = context.scene.sequence_editor.active_strip strip_dir = opsdata.get_strip_folder(active_strip) - shot_frames_dir = opsdata.get_shot_frames_dir(active_strip) + frames_root_dir = opsdata.get_frames_root_dir(active_strip) layout.separator() layout.row(align=True).label(text="From Farm Output:", icon="RENDER_ANIMATION") @@ -618,7 +609,7 @@ class RR_OT_sqe_approve_render(bpy.types.Operator): layout.separator() layout.row(align=True).label(text="To Shot Frames:", icon="FILE_TICK") - layout.row(align=True).label(text=shot_frames_dir.as_posix()) + layout.row(align=True).label(text=frames_root_dir.as_posix()) layout.separator() layout.row(align=True).label(text="Update Shot Frames?") diff --git a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py index a1cbfc53..234733d9 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py +++ b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py @@ -26,7 +26,7 @@ from typing import Set, Union, Optional, List, Dict, Any, Tuple import bpy from . import vars, checksqe -from .. import prefs +from .. import prefs, cache from .exception import NoImageSequenceAvailableException from ..logger import LoggerFactory @@ -83,7 +83,7 @@ def get_valid_cs_sequences( else: sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all - if prefs.is_blender_kitsu_enabled(): + if cache.project_active_get(): valid_sequences = [ s @@ -96,11 +96,11 @@ def get_valid_cs_sequences( return valid_sequences -def get_shot_frames_dir(strip: bpy.types.Sequence) -> Path: +def get_frames_root_dir(strip: bpy.types.Sequence) -> Path: # sf = shot_frames | fo = farm_output. addon_prefs = prefs.addon_prefs_get(bpy.context) fo_dir = get_strip_folder(strip) - sf_dir = addon_prefs.shot_frames_dir / fo_dir.parent.relative_to(fo_dir.parents[3]) + sf_dir = addon_prefs.frames_root_dir / fo_dir.parent.relative_to(fo_dir.parents[3]) return sf_dir @@ -116,7 +116,7 @@ def get_shot_previews_path(strip: bpy.types.Sequence) -> Path: # Fo > farm_output. addon_prefs = prefs.addon_prefs_get(bpy.context) fo_dir = get_strip_folder(strip) - shot_previews_dir = addon_prefs.shot_previews_path / fo_dir.parent.relative_to( + shot_previews_dir = addon_prefs.shot_playblast_root_dir / fo_dir.parent.relative_to( fo_dir.parents[3] ) @@ -172,12 +172,12 @@ def get_best_preview_sequence(dir: Path) -> List[Path]: def get_shot_frames_backup_path(strip: bpy.types.Sequence) -> Path: - fs_dir = get_shot_frames_dir(strip) + fs_dir = get_frames_root_dir(strip) return fs_dir.parent / f"_backup.{fs_dir.name}" def get_shot_frames_metadata_path(strip: bpy.types.Sequence) -> Path: - fs_dir = get_shot_frames_dir(strip) + fs_dir = get_frames_root_dir(strip) return fs_dir.parent / "metadata.json" diff --git a/scripts-blender/addons/blender_kitsu/render_review/ui.py b/scripts-blender/addons/blender_kitsu/render_review/ui.py index c6d82772..baa06a47 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ui.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ui.py @@ -34,8 +34,8 @@ from .ops import ( RR_OT_open_path, RR_OT_sqe_push_to_edit, ) -from . import opsdata, kitsu -from .. import prefs +from . import opsdata, core +from .. import prefs, cache class RR_PT_render_review(bpy.types.Panel): @@ -81,15 +81,14 @@ class RR_PT_render_review(bpy.types.Panel): row.prop(addon_prefs, 'use_video_latest_only') # Warning if kitsu on but not logged in. - if addon_prefs.enable_blender_kitsu and prefs.is_blender_kitsu_enabled(): - if not kitsu.is_auth(): - row = box.split(align=True, factor=0.7) - row.label(text="Kitsu enabled but not logged in", icon="ERROR") - row.operator("kitsu.session_start", text="Login") + if not prefs.session_auth(context): + row = box.split(align=True, factor=0.7) + row.label(text="Kitsu enabled but not logged in", icon="ERROR") + row.operator("kitsu.session_start", text="Login") - elif not kitsu.is_active_project(): - row = box.row(align=True) - row.label(text="Kitsu enabled but no active project", icon="ERROR") + elif not core.is_active_project(): + row = box.row(align=True) + row.label(text="Kitsu enabled but no active project", icon="ERROR") sqe = context.scene.sequence_editor if not sqe: @@ -134,7 +133,7 @@ class RR_PT_render_review(bpy.types.Panel): row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH") # Push to edit. - if not addon_prefs.shot_previews_path: + if not addon_prefs.shot_playblast_root_dir: shot_previews_dir = "" # ops handle invalid path else: shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix() @@ -145,9 +144,8 @@ class RR_PT_render_review(bpy.types.Panel): shot_previews_dir ) - if prefs.is_blender_kitsu_enabled(): - # Push strip to Kitsu. - box.row().operator('kitsu.sqe_push_shot', icon='URL') + # Push strip to Kitsu. + box.row().operator('kitsu.sqe_push_shot', icon='URL') def RR_topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None: -- 2.30.2 From f952efe5e3de80b5faf300158a83cb7a02ca11e6 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:43 -0400 Subject: [PATCH 03/17] Improve Render Review Operator Poll Messages --- .../addons/blender_kitsu/render_review/ops.py | 66 +++++++++++++++---- .../addons/blender_kitsu/sqe/ops.py | 1 + 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index 3630d1d3..432211f8 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -431,7 +431,16 @@ class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): active_strip = context.scene.sequence_editor.active_strip image_editor = opsdata.get_image_editor(context) - if not (active_strip and active_strip.rr.is_render and image_editor): + if not active_strip: + cls.poll_message_set("No sequence strip selected") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if not image_editor: + cls.poll_message_set("No image editor open in the current workspace") return False output_dir = opsdata.get_strip_folder(active_strip) @@ -440,6 +449,9 @@ class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): if f.is_file() and f.suffix == ".exr": return True + cls.poll_message_set("Selected strip is not EXR sequence") + return False + def execute(self, context: bpy.types.Context) -> Set[str]: active_strip = context.scene.sequence_editor.active_strip image_editor = opsdata.get_image_editor(context) @@ -489,7 +501,13 @@ class RR_OT_sqe_clear_exr_inspect(bpy.types.Operator): @classmethod def poll(cls, context: bpy.types.Context) -> bool: image_editor = cls._get_image_editor(context) - return bool(image_editor and image_editor.spaces.active.image) + if not image_editor: + cls.poll_message_set("No image editor open in the current workspace") + return False + if not image_editor.spaces.active.image: + cls.poll_message_set("No image to clear from image editor") + return False + return True def execute(self, context: bpy.types.Context) -> Set[str]: image_editor = self._get_image_editor(context) @@ -521,12 +539,23 @@ class RR_OT_sqe_approve_render(bpy.types.Operator): active_strip = context.scene.sequence_editor.active_strip addon_prefs = prefs.addon_prefs_get(bpy.context) - return bool( - addon_prefs.is_shot_frames_valid - and active_strip - and active_strip.rr.is_render - and not active_strip.rr.is_approved - ) + if not addon_prefs.shot_playblast_root_dir: + cls.poll_message_set("Playblast directory not set") + return False + + if not active_strip: + cls.poll_message_set("No sequence strip selected") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if active_strip.rr.is_approved: + cls.poll_message_set("Selected sequence strip is already approved") + return False + + return True def execute(self, context: bpy.types.Context) -> Set[str]: active_strip = context.scene.sequence_editor.active_strip @@ -764,13 +793,24 @@ class RR_OT_sqe_push_to_edit(bpy.types.Operator): @classmethod def poll(cls, context: bpy.types.Context) -> bool: addon_prefs = prefs.addon_prefs_get(context) - if not addon_prefs.shot_previews_path: + active_strip = context.scene.sequence_editor.active_strip + + if not addon_prefs.shot_playblast_root_dir: + cls.poll_message_set("No shot playblast root dir set") return False - active_strip = context.scene.sequence_editor.active_strip - return bool( - active_strip and active_strip.rr.is_render and not active_strip.rr.is_pushed_to_edit - ) + if not active_strip: + cls.poll_message_set("No active strip") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if not active_strip.rr.is_pushed_to_edit: + cls.poll_message_set("Selected sequence strip is already pushed to edit") + return False + return True def execute(self, context: bpy.types.Context) -> Set[str]: active_strip = context.scene.sequence_editor.active_strip diff --git a/scripts-blender/addons/blender_kitsu/sqe/ops.py b/scripts-blender/addons/blender_kitsu/sqe/ops.py index 7bf8e83e..14780ae1 100644 --- a/scripts-blender/addons/blender_kitsu/sqe/ops.py +++ b/scripts-blender/addons/blender_kitsu/sqe/ops.py @@ -1517,6 +1517,7 @@ class KITSU_OT_sqe_push_shot(bpy.types.Operator): def poll(cls, context: bpy.types.Context) -> bool: active_strip = context.scene.sequence_editor.active_strip if not hasattr(active_strip, 'filepath'): + cls.poll_message_set("Selected Strip is not a Video") return False return bool(prefs.session_auth(context)) -- 2.30.2 From 46e4198b2ef18f40f2d2b219bb349caaa85488a0 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:43 -0400 Subject: [PATCH 04/17] Remove no-op Code --- scripts-blender/addons/blender_kitsu/render_review/ops.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index 432211f8..085bacec 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -30,7 +30,6 @@ import bpy from . import vars, opsdata, util, core from .. import prefs, cache -from .. import types as kitsu_types from ..logger import LoggerFactory -- 2.30.2 From 5f4efaafab8778d88711523c7622f767ebbe8d47 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:43 -0400 Subject: [PATCH 05/17] De-Duplicate code for creating metadata strips --- .../blender_kitsu/render_review/core.py | 37 ------------------- .../addons/blender_kitsu/render_review/ops.py | 16 +++++++- .../addons/blender_kitsu/sqe/ops.py | 27 ++++---------- .../addons/blender_kitsu/sqe/opsdata.py | 24 +++++++++++- 4 files changed, 45 insertions(+), 59 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/render_review/core.py b/scripts-blender/addons/blender_kitsu/render_review/core.py index 0b6f66cc..3c5c81a4 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/core.py +++ b/scripts-blender/addons/blender_kitsu/render_review/core.py @@ -34,43 +34,6 @@ def is_active_project() -> bool: return bool(cache.project_active_get()) -# TODO De-duplicate code from sqe create metastrip -def create_metadata_strip( - context: bpy.types.Context, strip: bpy.types.Sequence -) -> bpy.types.MovieSequence: - # Get frame range information from current strip. - strip_range = range(strip.frame_final_start, strip.frame_final_end) - channel = strip.channel + 1 - - addon_prefs = prefs.addon_prefs_get(context) - # Create new metadata strip. - metadata_strip = context.scene.sequence_editor.sequences.new_movie( - f"{strip.name}_metadata-strip", - addon_prefs.metadatastrip_file, - strip.channel + 1, - strip.frame_final_start, - ) - - # Set blend alpha. - metadata_strip.blend_alpha = 0 - - # Set frame in and out. - metadata_strip.frame_final_start = strip.frame_final_start - metadata_strip.frame_final_end = strip.frame_final_end - # Metadata_strip.channel = strip.channel + 1. - - # Init start frame offset. - opsdata.init_start_frame_offset(metadata_strip) - - logger.info( - "%s created Metadata Strip: %s", - strip.name, - metadata_strip.name, - ) - - return metadata_strip - - # TODO De-duplicate code from sqe code def link_strip_by_name( context: bpy.types.Context, diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index 085bacec..c0e266a3 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -30,7 +30,7 @@ import bpy from . import vars, opsdata, util, core from .. import prefs, cache - +from ..sqe import opsdata as seq_opsdata from ..logger import LoggerFactory from .exception import NoImageSequenceAvailableException @@ -149,7 +149,19 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): sequence_name = shot_version_folders[0].parent.parent.parent.name # Create metadata strip. - metadata_strip = core.create_metadata_strip(context, strip_longest) + metadata_strip = seq_opsdata.create_metadata_strip( + context.scene, + f"{strip_longest.name}_metadata-strip", + strip_longest.channel + 1, + strip_longest.frame_final_start, + strip_longest.frame_final_end, + ) + + logger.info( + "%s created Metadata Strip: %s", + strip_longest.name, + metadata_strip.name, + ) # Link metadata strip. core.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) diff --git a/scripts-blender/addons/blender_kitsu/sqe/ops.py b/scripts-blender/addons/blender_kitsu/sqe/ops.py index 14780ae1..c5b48e74 100644 --- a/scripts-blender/addons/blender_kitsu/sqe/ops.py +++ b/scripts-blender/addons/blender_kitsu/sqe/ops.py @@ -1806,13 +1806,10 @@ class KITSU_OT_sqe_pull_edit(bpy.types.Operator): # TODO Refactor as this reuses code from KITSU_OT_sqe_create_metadata_strip if not strip: # Create new strip. - strip = context.scene.sequence_editor.sequences.new_movie( - shot.name, - addon_prefs.metadatastrip_file, - channel, - frame_start, + + strip = opsdata.create_metadata_strip( + context.scene, shot.name, channel, frame_start, frame_end ) - strip.frame_final_end = frame_end # Apply slip to match offset. self._apply_strip_slip_from_shot(context, strip, shot) @@ -2011,25 +2008,17 @@ class KITSU_OT_sqe_create_metadata_strip(bpy.types.Operator): # Create new metadata strip. # TODO: frame range of metadata strip is 1000 which is problematic because it needs to fit # on the first try, EDIT: seems to work maybe per python overlaps of sequences possible? - metadata_strip = context.scene.sequence_editor.sequences.new_movie( + + metadata_strip = opsdata.create_metadata_strip( + context.scene, f"{strip.name}{bkglobals.DELIMITER}metadata{bkglobals.SPACE_REPLACER}strip", - addon_prefs.metadatastrip_file, strip.channel + 1, strip.frame_final_start, + strip.frame_final_end, ) + created.append(metadata_strip) - # Set blend alpha. - metadata_strip.blend_alpha = 0 - - # Set frame in and out. - metadata_strip.frame_final_start = strip.frame_final_start - metadata_strip.frame_final_end = strip.frame_final_end - metadata_strip.channel = strip.channel + 1 - - # Init start frame offst. - opsdata.init_start_frame_offset(metadata_strip) - logger.info( "%s created metadata strip: %s", strip.name, diff --git a/scripts-blender/addons/blender_kitsu/sqe/opsdata.py b/scripts-blender/addons/blender_kitsu/sqe/opsdata.py index 525ced46..43b8a3ff 100644 --- a/scripts-blender/addons/blender_kitsu/sqe/opsdata.py +++ b/scripts-blender/addons/blender_kitsu/sqe/opsdata.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Any, Dict, List, Tuple, Union, Optional import bpy -from .. import bkglobals +from .. import bkglobals, prefs from ..logger import LoggerFactory from ..types import Sequence, Task, TaskStatus, Shot, TaskType @@ -243,3 +243,25 @@ def push_sequence_color(context: bpy.types.Context, sequence: Sequence) -> None: else: sequence.update_data({"color": list(item.color)}) logger.info("%s pushed sequence color", sequence.name) + + +def create_metadata_strip( + scene: bpy.types.Scene, name: str, channel, frame_start: int, frame_end: int +) -> bpy.types.MovieSequence: + + addon_prefs = prefs.addon_prefs_get(bpy.context) + strip = scene.sequence_editor.sequences.new_movie( + name, + addon_prefs.metadatastrip_file, + channel, + frame_start, + ) + + strip.frame_final_end = frame_end + + # Set blend alpha. + strip.blend_alpha = 0 + + init_start_frame_offset(strip) + + return strip -- 2.30.2 From 466ba38a9f1f8029262a5bdcac730acf8dd42a6d Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 12:57:44 -0400 Subject: [PATCH 06/17] De-Duplicate Link Metadata Strip Code --- .../blender_kitsu/render_review/__init__.py | 2 - .../blender_kitsu/render_review/core.py | 69 ------------------- .../addons/blender_kitsu/render_review/ops.py | 6 +- .../blender_kitsu/render_review/opsdata.py | 34 ++++++++- .../addons/blender_kitsu/render_review/ui.py | 6 +- .../addons/blender_kitsu/sqe/ops.py | 9 +-- .../addons/blender_kitsu/sqe/opsdata.py | 16 ++++- 7 files changed, 55 insertions(+), 87 deletions(-) delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/core.py diff --git a/scripts-blender/addons/blender_kitsu/render_review/__init__.py b/scripts-blender/addons/blender_kitsu/render_review/__init__.py index 4741dacb..29d08bdc 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/__init__.py +++ b/scripts-blender/addons/blender_kitsu/render_review/__init__.py @@ -23,7 +23,6 @@ import bpy from . import ( util, props, - core, opsdata, checksqe, ops, @@ -40,7 +39,6 @@ if _need_reload: util = importlib.reload(util) props = importlib.reload(props) - core = importlib.reload(core) opsdata = importlib.reload(opsdata) checksqe = importlib.reload(checksqe) ops = importlib.reload(ops) diff --git a/scripts-blender/addons/blender_kitsu/render_review/core.py b/scripts-blender/addons/blender_kitsu/render_review/core.py deleted file mode 100644 index 3c5c81a4..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/core.py +++ /dev/null @@ -1,69 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -from typing import List, Dict, Union, Any, Set, Optional -import bpy - -from . import util -from .. import cache, types, prefs -from ..sqe import opsdata, pull, push - -from ..logger import LoggerFactory - -logger = LoggerFactory.getLogger() - - -def is_active_project() -> bool: - return bool(cache.project_active_get()) - - -# TODO De-duplicate code from sqe code -def link_strip_by_name( - context: bpy.types.Context, - strip: bpy.types.Sequence, - shot_name: str, - sequence_name: str, -) -> None: - # Get seq and shot. - active_project = cache.project_active_get() - seq = active_project.get_sequence_by_name(sequence_name) - shot = active_project.get_shot_by_name(seq, shot_name) - - if not shot: - logger.error("Unable to find shot %s on kitsu", shot_name) - return - - # Pull shot meta. - pull.shot_meta(strip, shot) - - # Rename strip. - strip.name = shot.name - - # Pull sequence color. - opsdata.append_sequence_color(context, seq) - - # Log. - t = "Linked strip: %s to shot: %s with ID: %s" % ( - strip.name, - shot.name, - shot.id, - ) - logger.info(t) - util.redraw_ui() diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index c0e266a3..ffeb01b5 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -28,7 +28,7 @@ from collections import OrderedDict import bpy -from . import vars, opsdata, util, core +from . import vars, opsdata, util from .. import prefs, cache from ..sqe import opsdata as seq_opsdata from ..logger import LoggerFactory @@ -145,7 +145,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): # Perform kitsu operations if enabled. if prefs.session_auth(context) and imported_strips: - if core.is_active_project(): + if opsdata.is_active_project(): sequence_name = shot_version_folders[0].parent.parent.parent.name # Create metadata strip. @@ -164,7 +164,7 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): ) # Link metadata strip. - core.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) + opsdata.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) else: logger.error( diff --git a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py index 234733d9..231b77d8 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py +++ b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py @@ -25,8 +25,9 @@ from typing import Set, Union, Optional, List, Dict, Any, Tuple import bpy -from . import vars, checksqe +from . import vars, checksqe, util from .. import prefs, cache +from ..sqe import opsdata as sqe_opsdata from .exception import NoImageSequenceAvailableException from ..logger import LoggerFactory @@ -397,3 +398,34 @@ def setup_color_management(context: bpy.types.Context) -> None: if context.scene.view_settings.view_transform != 'Standard': context.scene.view_settings.view_transform = 'Standard' logger.info("Set view transform to: Standard") + + +def is_active_project() -> bool: + return bool(cache.project_active_get()) + + +def link_strip_by_name( + context: bpy.types.Context, + strip: bpy.types.Sequence, + shot_name: str, + sequence_name: str, +) -> None: + # Get seq and shot. + active_project = cache.project_active_get() + seq = active_project.get_sequence_by_name(sequence_name) + shot = active_project.get_shot_by_name(seq, shot_name) + + if not shot: + logger.error("Unable to find shot %s on kitsu", shot_name) + return + + sqe_opsdata.link_metadata_strip(context, shot, seq, strip) + + # Log. + t = "Linked strip: %s to shot: %s with ID: %s" % ( + strip.name, + shot.name, + shot.id, + ) + logger.info(t) + util.redraw_ui() diff --git a/scripts-blender/addons/blender_kitsu/render_review/ui.py b/scripts-blender/addons/blender_kitsu/render_review/ui.py index baa06a47..91c73e15 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ui.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ui.py @@ -34,8 +34,8 @@ from .ops import ( RR_OT_open_path, RR_OT_sqe_push_to_edit, ) -from . import opsdata, core -from .. import prefs, cache +from . import opsdata +from .. import prefs class RR_PT_render_review(bpy.types.Panel): @@ -86,7 +86,7 @@ class RR_PT_render_review(bpy.types.Panel): row.label(text="Kitsu enabled but not logged in", icon="ERROR") row.operator("kitsu.session_start", text="Login") - elif not core.is_active_project(): + elif not opsdata.is_active_project(): row = box.row(align=True) row.label(text="Kitsu enabled but no active project", icon="ERROR") diff --git a/scripts-blender/addons/blender_kitsu/sqe/ops.py b/scripts-blender/addons/blender_kitsu/sqe/ops.py index c5b48e74..1acb5f01 100644 --- a/scripts-blender/addons/blender_kitsu/sqe/ops.py +++ b/scripts-blender/addons/blender_kitsu/sqe/ops.py @@ -592,15 +592,8 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator): self.report({"WARNING"}, "ID not found on server: %s" % shot_id) return {"CANCELLED"} - # Pull shot meta. - pull.shot_meta(self._strip, shot) - - # Rename strip. - self._strip.name = shot.name - - # Pull sequence color. seq = Sequence.by_id(shot.parent_id) - opsdata.append_sequence_color(context, seq) + opsdata.link_metadata_strip(context, shot, seq, self._strip) # Log. t = "Linked strip: %s to shot: %s with ID: %s" % ( diff --git a/scripts-blender/addons/blender_kitsu/sqe/opsdata.py b/scripts-blender/addons/blender_kitsu/sqe/opsdata.py index 43b8a3ff..0a05cd02 100644 --- a/scripts-blender/addons/blender_kitsu/sqe/opsdata.py +++ b/scripts-blender/addons/blender_kitsu/sqe/opsdata.py @@ -21,7 +21,7 @@ import re from pathlib import Path from typing import Any, Dict, List, Tuple, Union, Optional - +from . import pull import bpy from .. import bkglobals, prefs from ..logger import LoggerFactory @@ -265,3 +265,17 @@ def create_metadata_strip( init_start_frame_offset(strip) return strip + + +def link_metadata_strip( + context, shot: Shot, seq: Sequence, strip: bpy.types.MovieSequence +) -> bpy.types.MovieSequence: + # Pull shot meta. + pull.shot_meta(strip, shot) + + # Rename strip. + strip.name = shot.name + + # Pull sequence color. + append_sequence_color(context, seq) + return strip -- 2.30.2 From f7a78d9a660d7ede1aa5cf043bdb51166d8558ad Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 13:08:48 -0400 Subject: [PATCH 07/17] Render Review, add early return if farm output directory doesn't exist --- .../addons/blender_kitsu/render_review/ops.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index ffeb01b5..54846eb0 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -399,6 +399,15 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): layout.prop(addon_prefs, 'shot_name_filter') def execute(self, context: bpy.types.Context) -> Set[str]: + render_dir = context.scene.rr.render_dir + if not Path(render_dir).exists(): + self.report( + {"ERROR"}, + f"Farm Output Directory {render_dir} doesn't exist, check Add-On preferences", + ) + + return {"CANCELLED"} + scripts_path = bpy.utils.script_paths(use_user=False)[0] template_path = "/startup/bl_app_templates_system/Video_Editing/startup.blend" ws_filepath = Path(scripts_path + template_path) -- 2.30.2 From dd186d798d353e504af0221b742965260c823977 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 13:16:46 -0400 Subject: [PATCH 08/17] Auto-Fill Farm Directory --- scripts-blender/addons/blender_kitsu/prefs.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index ac330794..2e7397b0 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -469,11 +469,26 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): # Render Review #################### + def set_farm_dir(self, input): + self['farm_output_dir'] = input + return + + def get_farm_dir( + self, + ) -> str: + if get_safely_string_prop(self, 'farm_output_dir') == "" and self.project_root_path: + dir = self.project_root_path.joinpath("render/") + if dir.exists(): + return dir.as_posix() + return get_safely_string_prop(self, 'farm_output_dir') + farm_output_dir: bpy.props.StringProperty( # type: ignore name="Farm Output Directory", - description="Should point to: /render/sprites/farm_output", - default="/render/sprites/farm_output", + description="Directory used as 'Render Output Root' when submitting job to Flamenco. Usually points to: {project}/render/", + default="", subtype="DIR_PATH", + get=get_farm_dir, + set=set_farm_dir, ) shot_name_filter: bpy.props.StringProperty( # type: ignore -- 2.30.2 From dbd8197e7c701f148c38d30258de52e833861892 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 13:59:52 -0400 Subject: [PATCH 09/17] Re-Organize Add-On Preferences --- scripts-blender/addons/blender_kitsu/prefs.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index 2e7397b0..7fc592c7 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -575,6 +575,25 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): file_pattern_row.prop(self, "edit_export_file_pattern", text="Export File Pattern") box.row().prop(self, "edit_export_frame_offset") + # Render Review + self.draw_render_review(col) + + # Shot_Builder settings. + box = col.box() + box.label(text="Shot Builder", icon="MOD_BUILD") + box.prop(self, "shot_builder_frame_offset") + row = box.row(align=True) + # Avoids circular import error + from .shot_builder.ops import ( + KITSU_OT_build_config_save_settings, + KITSU_OT_build_config_save_hooks, + KITSU_OT_build_config_save_templates, + ) + + box.row().operator(KITSU_OT_build_config_save_hooks.bl_idname, icon='FILE_SCRIPT') + box.row().operator(KITSU_OT_build_config_save_settings.bl_idname, icon="TEXT") + box.row().operator(KITSU_OT_build_config_save_templates.bl_idname, icon="FILE_BLEND") + # Lookdev tools settings. self.lookdev.draw(context, col) @@ -603,24 +622,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): emboss=False, ) - # Shot_Builder settings. - box = col.box() - box.label(text="Shot Builder", icon="MOD_BUILD") - box.prop(self, "shot_builder_frame_offset") - row = box.row(align=True) - # Avoids circular import error - from .shot_builder.ops import ( - KITSU_OT_build_config_save_settings, - KITSU_OT_build_config_save_hooks, - KITSU_OT_build_config_save_templates, - ) - box.row().operator(KITSU_OT_build_config_save_hooks.bl_idname, icon='FILE_SCRIPT') - box.row().operator(KITSU_OT_build_config_save_settings.bl_idname, icon="TEXT") - box.row().operator(KITSU_OT_build_config_save_templates.bl_idname, icon="FILE_BLEND") - - # Render Review - self.draw_render_review(col) - # Misc settings. box = col.box() box.label(text="Miscellaneous", icon="MODIFIER") -- 2.30.2 From 8275202ce3f1e82bafe3aec114cb5f1a69ccaa1f Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 14:08:27 -0400 Subject: [PATCH 10/17] Update Add-On Preferences Documentation --- docs/media/td-guide/kitsu_pref.jpg | 4 ++-- docs/td-guide/addon_preferences.md | 30 ++++++++++++------------------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/docs/media/td-guide/kitsu_pref.jpg b/docs/media/td-guide/kitsu_pref.jpg index a66ea98c..845e279d 100644 --- a/docs/media/td-guide/kitsu_pref.jpg +++ b/docs/media/td-guide/kitsu_pref.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0615cd06ebcf541bc61431b2fc2cddf1a31fb2a671b23e5698f246f2fa4b7f6d -size 106424 +oid sha256:9df326f8d3c5c3519432887e6b5c94c54d55abf5923ca9c5d802748d6a44f280 +size 20915 diff --git a/docs/td-guide/addon_preferences.md b/docs/td-guide/addon_preferences.md index d9352f35..9adfb95d 100644 --- a/docs/td-guide/addon_preferences.md +++ b/docs/td-guide/addon_preferences.md @@ -12,24 +12,18 @@ - Password: `{user_password}` - Project Settings - Select Production: Choose the current Production - - Project Root Directory: `data/your_project_name/svn` - - Animation Tools - - Playblast directory: `data/your_project_name/shared/editorial/footage/pro/` - - Frames Directory: `data/your_project_name/shared/editorial/footage/post/` - - Editorial Export Directory (Optional) - - `data/your_project_name/shared/editorial/export/` - + - Project Root Directory: `data/your_project_name` + + ![Blender Kitsu Preferences](/media/td-guide/kitsu_pref.jpg) -## Render Review Add-On Preferences - 1. Open Blender and Select `Edit>Preferences>Add-Ons` - 2. Search the 'Render Review' and use the checkbox to Enable the Add-On - 3. Set the following settings in the add-on preferences - - Ensure `Enable Blender Kitsu` is Enabled - - Render Farm: `data/your_project_name/render/` - - Shot Frames: `data/your_project_name/shared/editorial/footage/post/` - - Shot Previews: `data/your_project_name/shared/editorial/footage/pro/` +The following settings will be automatically set if they exist. You can set a custom path by manually entering them, or leave them blank to set them automatically. -![Render Review Preferences](/media/td-guide/render_review_pref.jpg) \ No newline at end of file +- Animation Tools + - Sequence Playblasts: `data/your_project_name/shared/editorial/footage/pre/` + - Shot Playblasts: `data/your_project_name/shared/editorial/footage/pro/` + - Frames Directory: `data/your_project_name/shared/editorial/footage/post/` +- Editorial + - Export Directory `data/your_project_name/shared/editorial/export/` +- Render Review + - Farm Output Directory `data/your_project_name/render/` \ No newline at end of file -- 2.30.2 From f5432a3c9163acf8f12e6f04ad0afd13f574d35a Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 14:20:27 -0400 Subject: [PATCH 11/17] Clarify 'offline' mode for Render Review Add-On --- scripts-blender/addons/blender_kitsu/README.md | 2 +- scripts-blender/addons/blender_kitsu/render_review/README.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/README.md diff --git a/scripts-blender/addons/blender_kitsu/README.md b/scripts-blender/addons/blender_kitsu/README.md index b78c0aa8..4f2862aa 100644 --- a/scripts-blender/addons/blender_kitsu/README.md +++ b/scripts-blender/addons/blender_kitsu/README.md @@ -453,7 +453,7 @@ expected. - Push a render to the edit which uses the existing .mp4 preview or creates it with ffmpeg and copies it over to the shot_preview folder with automatic versioning incrementation - Creation of metadata.json files on approving renders and pushing renders to edit to keep track where a file came from -- Connection to `blender-kitsu` addon, that can be enabled and extends the functionality of some operators +- Load Sequences without Kitsu server when no project is set in Blender Kitsu Add-On ### Links diff --git a/scripts-blender/addons/blender_kitsu/render_review/README.md b/scripts-blender/addons/blender_kitsu/render_review/README.md deleted file mode 100644 index e69de29b..00000000 -- 2.30.2 From a0e5201b592de305124c027090038708e8308000 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Fri, 17 May 2024 14:21:33 -0400 Subject: [PATCH 12/17] Remove Render Review from Add-On table --- scripts-blender/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts-blender/README.md b/scripts-blender/README.md index c7aa1251..a7810b51 100644 --- a/scripts-blender/README.md +++ b/scripts-blender/README.md @@ -19,5 +19,4 @@ Download release packages of the below add-ons from the [Releases Page](https:// |Grease Converter |Convert annotations to Grease Pencil objects and vise versa. |Lattice Magic |Lattice-based utilities. |Lighting Overrider |Create, manage and apply python overrides in a flexible and reliable way. -|Pose Shape Keys |Manage and maintain shapekeys for rigging. -|Render Review |Review renders from Flamenco with the sequence editor. +|Pose Shape Keys |Manage and maintain shapekeys for rigging. \ No newline at end of file -- 2.30.2 From 9015839fffd7e63e82c9d04e3a6418987c9c001b Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Sat, 25 May 2024 12:16:35 -0400 Subject: [PATCH 13/17] Add feature to skip incomplete renders --- scripts-blender/addons/blender_kitsu/prefs.py | 9 +++++++++ .../addons/blender_kitsu/render_review/ops.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index 7fc592c7..f3d489cc 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -491,6 +491,9 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): set=set_farm_dir, ) + ########################## + # Render Review Settings + ########################## shot_name_filter: bpy.props.StringProperty( # type: ignore name="Shot Name Filter", description="Shot name must include this string, otherwise it will be ignored", @@ -506,6 +509,12 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): description="Only load video files for the latest versions by default, to avoid running out of memory and crashing", ) + skip_incomplete_renders: bpy.props.BoolProperty( # type:ignore + default=False, + name="Skip Incomplete Renders", + description="Skip renders that are shorter than the longest render for a give shot, (i.e. missing frames)", + ) + versions_max_count: bpy.props.IntProperty( name="Max Versions", description="Desired number of versions to load for each shot", diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index 54846eb0..96e7d97f 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -143,6 +143,11 @@ class RR_OT_sqe_create_review_session(bpy.types.Operator): strip_longest = imported_strips[-1] prev_frame_end = strip_longest.frame_final_end + if addon_prefs.skip_incomplete_renders: + for strip in imported_strips: + if strip.frame_final_duration < strip_longest.frame_final_duration: + context.scene.sequence_editor.sequences.remove(strip) + # Perform kitsu operations if enabled. if prefs.session_auth(context) and imported_strips: if opsdata.is_active_project(): @@ -391,6 +396,7 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): layout.prop(self, 'sequence') if self.sequence != 'None': + layout.row().prop(addon_prefs, 'skip_incomplete_renders') row = layout.row() row.prop(addon_prefs, 'use_video') if addon_prefs.use_video: -- 2.30.2 From dbf307aad4e612e46261b52c4e522e17fade4b9f Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Sat, 25 May 2024 12:17:36 -0400 Subject: [PATCH 14/17] Fix bug in early return for farm output directory --- .../addons/blender_kitsu/render_review/ops.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py index 96e7d97f..1d21ab44 100644 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -406,13 +406,6 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): def execute(self, context: bpy.types.Context) -> Set[str]: render_dir = context.scene.rr.render_dir - if not Path(render_dir).exists(): - self.report( - {"ERROR"}, - f"Farm Output Directory {render_dir} doesn't exist, check Add-On preferences", - ) - - return {"CANCELLED"} scripts_path = bpy.utils.script_paths(use_user=False)[0] template_path = "/startup/bl_app_templates_system/Video_Editing/startup.blend" @@ -425,6 +418,13 @@ class RR_OT_setup_review_workspace(bpy.types.Operator): # Pre-fill render directory with farm output shots directory. addon_prefs = prefs.addon_prefs_get(bpy.context) context.scene.rr.render_dir = addon_prefs.farm_output_dir + "/shots" + if not Path(render_dir).exists(): + self.report( + {"ERROR"}, + f"Farm Output Directory {render_dir} doesn't exist, check Add-On preferences", + ) + + return {"CANCELLED"} # Init sqe. if not context.scene.sequence_editor: -- 2.30.2 From 321f919639cb2669255158ca86640cf438f17889 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Mon, 27 May 2024 14:02:30 -0400 Subject: [PATCH 15/17] Skip Incomplete Renders by Default --- scripts-blender/addons/blender_kitsu/prefs.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index f3d489cc..2e0e74f4 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -491,9 +491,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): set=set_farm_dir, ) - ########################## - # Render Review Settings - ########################## shot_name_filter: bpy.props.StringProperty( # type: ignore name="Shot Name Filter", description="Shot name must include this string, otherwise it will be ignored", @@ -509,8 +506,8 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): description="Only load video files for the latest versions by default, to avoid running out of memory and crashing", ) - skip_incomplete_renders: bpy.props.BoolProperty( # type:ignore - default=False, + skip_incomplete_renders: bpy.props.BoolProperty( # type: ignore + default=True, name="Skip Incomplete Renders", description="Skip renders that are shorter than the longest render for a give shot, (i.e. missing frames)", ) -- 2.30.2 From 681b190462371151348887749b79fc19d35f8b67 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Mon, 27 May 2024 14:04:01 -0400 Subject: [PATCH 16/17] Remove old Render Review Folder --- .../blender_kitsu/render_review/__init__.py | 60 - .../blender_kitsu/render_review/checksqe.py | 94 -- .../blender_kitsu/render_review/draw.py | 263 ----- .../blender_kitsu/render_review/exception.py | 25 - .../addons/blender_kitsu/render_review/ops.py | 1043 ----------------- .../blender_kitsu/render_review/opsdata.py | 431 ------- .../blender_kitsu/render_review/props.py | 102 -- .../addons/blender_kitsu/render_review/ui.py | 178 --- .../blender_kitsu/render_review/util.py | 44 - .../blender_kitsu/render_review/vars.py | 25 - 10 files changed, 2265 deletions(-) delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/__init__.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/checksqe.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/draw.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/exception.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/ops.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/opsdata.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/props.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/ui.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/util.py delete mode 100644 scripts-blender/addons/blender_kitsu/render_review/vars.py diff --git a/scripts-blender/addons/blender_kitsu/render_review/__init__.py b/scripts-blender/addons/blender_kitsu/render_review/__init__.py deleted file mode 100644 index 29d08bdc..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import bpy - -from . import ( - util, - props, - opsdata, - checksqe, - ops, - ui, - draw, -) - - -_need_reload = "ops" in locals() - - -if _need_reload: - import importlib - - util = importlib.reload(util) - props = importlib.reload(props) - opsdata = importlib.reload(opsdata) - checksqe = importlib.reload(checksqe) - ops = importlib.reload(ops) - ui = importlib.reload(ui) - draw = importlib.reload(draw) - - -def register(): - props.register() - ops.register() - ui.register() - draw.register() - - -def unregister(): - draw.unregister() - ui.unregister() - ops.unregister() - props.unregister() diff --git a/scripts-blender/addons/blender_kitsu/render_review/checksqe.py b/scripts-blender/addons/blender_kitsu/render_review/checksqe.py deleted file mode 100644 index d6950408..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/checksqe.py +++ /dev/null @@ -1,94 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -from typing import Dict, List, Set, Optional, Tuple, Any - -import bpy - - -def _do_ranges_collide(range1: range, range2: range) -> bool: - """Whether the two ranges collide with each other .""" - # usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140) - # first and last frame can be the same for each strip - range2 = range(range2.start + 1, range2.stop - 1) - - if not range1: - return True # empty range is subset of anything - - if not range2: - return False # non-empty range can't be subset of empty range - - if len(range1) > 1 and range1.step % range2.step: - return False # must have a single value or integer multiple step - - if range(range1.start + 1, range1.stop - 1) == range2: - return True - - if range2.start in range1 or range2[-1] in range1: - return True - - return range1.start in range2 or range1[-1] in range2 - - -def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]: - """ - Scans sequence editor and returns a dictionary. It contains a key for each channel - and a list of ranges with the occupied frame ranges as values. - """ - # {'1': [(101, 213), (300, 320)]}. - ranges: Dict[str, List[range]] = {} - - # Populate ranges. - for strip in context.scene.sequence_editor.sequences_all: - ranges.setdefault(str(strip.channel), []) - ranges[str(strip.channel)].append( - range(strip.frame_final_start, strip.frame_final_end + 1) - ) - - # Sort ranges tuple list. - for channel in ranges: - liste = ranges[channel] - ranges[channel] = sorted(liste, key=lambda item: item.start) - - return ranges - - -def get_occupied_ranges_for_strips(sequences: List[bpy.types.Sequence]) -> List[range]: - """ - Scans input list of sequences and returns a list of ranges that represent the occupied frame ranges. - """ - ranges: List[range] = [] - - # Populate ranges. - for strip in sequences: - ranges.append(range(strip.frame_final_start, strip.frame_final_end + 1)) - - # Sort ranges tuple list. - ranges.sort(key=lambda item: item.start) - return ranges - - -def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool: - for r in occupied_ranges: - # Range(101, 150). - if _do_ranges_collide(range_to_check, r): - return True - continue - return False diff --git a/scripts-blender/addons/blender_kitsu/render_review/draw.py b/scripts-blender/addons/blender_kitsu/render_review/draw.py deleted file mode 100644 index 20676813..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/draw.py +++ /dev/null @@ -1,263 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# . - -# This file is copied from the blender-cloud-addon https://developer.blender.org/diffusion/BCA/ -# Author of this file is: Sybren A. Stuevel -# Modified by: Paul Golter - -import typing - -import bpy -import gpu - -APPROVED_COLOR = (0.24, 1, 0.139, 0.7) -PUSHED_TO_EDIT_COLOR = (0.8, .8, 0.1, 0.5) - -# Glsl. -gpu_vertex_shader = """ -uniform mat4 ModelViewProjectionMatrix; - -layout (location = 0) in vec2 pos; -layout (location = 1) in vec4 color; - -out vec4 lineColor; // output to the fragment shader - -void main() -{ - gl_Position = ModelViewProjectionMatrix * vec4(pos.x, pos.y, 0.0, 1.0); - lineColor = color; -} -""" - -gpu_fragment_shader = """ -out vec4 fragColor; -in vec4 lineColor; - -void main() -{ - fragColor = lineColor; -} -""" - -Float2 = typing.Tuple[float, float] -Float3 = typing.Tuple[float, float, float] -Float4 = typing.Tuple[float, float, float, float] -LINE_WIDTH = 6 - - -class LineDrawer: - def __init__(self): - self._format = gpu.types.GPUVertFormat() - self._pos_id = self._format.attr_add( - id="pos", comp_type="F32", len=2, fetch_mode="FLOAT" - ) - self._color_id = self._format.attr_add( - id="color", comp_type="F32", len=4, fetch_mode="FLOAT" - ) - - self.shader = gpu.types.GPUShader(gpu_vertex_shader, gpu_fragment_shader) - - def draw(self, coords: typing.List[Float2], colors: typing.List[Float4]): - global LINE_WIDTH - - if not coords: - return - gpu.state.blend_set("ALPHA") - gpu.state.line_width_set(LINE_WIDTH) - - vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format) - vbo.attr_fill(id=self._pos_id, data=coords) - vbo.attr_fill(id=self._color_id, data=colors) - - batch = gpu.types.GPUBatch(type="LINES", buf=vbo) - batch.program_set(self.shader) - batch.draw() - - -def get_strip_rectf(strip) -> Float4: - # Get x and y in terms of the grid's frames and channels. - x1 = strip.frame_final_start - x2 = strip.frame_final_end - # Seems to be a 5 % offset from channel top start of strip. - y1 = strip.channel + 0.05 - y2 = strip.channel - 0.05 + 1 - - return x1, y1, x2, y2 - - -def line_in_strip( - strip_coords: Float4, - pixel_size_x: float, - color: Float4, - line_height_factor: float, - out_coords: typing.List[Float2], - out_colors: typing.List[Float4], -): - # Strip coords. - s_x1, s_y1, s_x2, s_y2 = strip_coords - - # Calculate line height with factor. - line_y = (1 - line_height_factor) * s_y1 + line_height_factor * s_y2 - - # if strip is shorter than line_width use stips s_x2 - # line_x2 = s_x1 + line_width if (s_x2 - s_x1 > line_width) else s_x2 - line_x2 = s_x2 - - # Be careful not to draw over the current frame line. - cf_x = bpy.context.scene.frame_current_final - - # TODO(Sybren): figure out how to pass one colour per line, - # instead of one colour per vertex. - out_coords.append((s_x1, line_y)) - out_colors.append(color) - - if s_x1 < cf_x < line_x2: - # Bad luck, the line passes our strip, so draw two lines. - out_coords.append((cf_x - pixel_size_x, line_y)) - out_colors.append(color) - - out_coords.append((cf_x + pixel_size_x, line_y)) - out_colors.append(color) - - out_coords.append((line_x2, line_y)) - out_colors.append(color) - - -def draw_callback_px(line_drawer: LineDrawer): - global LINE_WIDTH - - context = bpy.context - - if not context.scene.sequence_editor: - return - - # From . import shown_strips. - - region = context.region - xwin1, ywin1 = region.view2d.region_to_view(0, 0) - xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height) - one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1) - pixel_size_x = one_pixel_further_x - xwin1 - - # Strips = shown_strips(context). - strips = context.scene.sequence_editor.sequences_all - - coords = [] # type: typing.List[Float2] - colors = [] # type: typing.List[Float4] - - # Collect all the lines (vertex coords + vertex colours) to draw. - for strip in strips: - - # Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords. - strip_coords = get_strip_rectf(strip) - - # Check if any of the coordinates are out of bounds. - if ( - strip_coords[0] > xwin2 - or strip_coords[2] < xwin1 - or strip_coords[1] > ywin2 - or strip_coords[3] < ywin1 - ): - continue - - if strip.rr.is_approved: - line_in_strip( - strip_coords, - pixel_size_x, - APPROVED_COLOR, - 0.05, - coords, - colors, - ) - elif strip.rr.is_pushed_to_edit: - line_in_strip( - strip_coords, - pixel_size_x, - PUSHED_TO_EDIT_COLOR, - 0.05, - coords, - colors, - ) - - line_drawer.draw(coords, colors) - - -def tag_redraw_all_sequencer_editors(): - context = bpy.context - - # Py cant access notifiers. - for window in context.window_manager.windows: - for area in window.screen.areas: - if area.type == "SEQUENCE_EDITOR": - for region in area.regions: - if region.type == "WINDOW": - region.tag_redraw() - - -# This is a list so it can be changed instead of set -# if it is only changed, it does not have to be declared as a global everywhere -cb_handle = [] - - -def callback_enable(): - global cb_handle - - if cb_handle: - return - - # Doing GPU stuff in the background crashes Blender, so let's not. - if bpy.app.background: - return - - line_drawer = LineDrawer() - cb_handle[:] = ( - bpy.types.SpaceSequenceEditor.draw_handler_add( - draw_callback_px, (line_drawer,), "WINDOW", "POST_VIEW" - ), - ) - - tag_redraw_all_sequencer_editors() - - -def callback_disable(): - global cb_handle - - if not cb_handle: - return - - try: - bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], "WINDOW") - except ValueError: - # Thrown when already removed. - pass - cb_handle.clear() - - tag_redraw_all_sequencer_editors() - - -# ---------REGISTER ----------. - - -def register(): - callback_enable() - - -def unregister(): - callback_disable() diff --git a/scripts-blender/addons/blender_kitsu/render_review/exception.py b/scripts-blender/addons/blender_kitsu/render_review/exception.py deleted file mode 100644 index 004c22fc..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/exception.py +++ /dev/null @@ -1,25 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - - -class NoImageSequenceAvailableException(Exception): - """ - Error raised when trying to gather image sequence in folder but no files are existent - """ diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py deleted file mode 100644 index 1d21ab44..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/ops.py +++ /dev/null @@ -1,1043 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import sys -import subprocess -import shutil -from pathlib import Path -from datetime import datetime -from typing import Set, Union, Optional, List, Dict, Any, Tuple -from collections import OrderedDict - -import bpy - -from . import vars, opsdata, util -from .. import prefs, cache -from ..sqe import opsdata as seq_opsdata -from ..logger import LoggerFactory - -from .exception import NoImageSequenceAvailableException - -logger = LoggerFactory.getLogger() - - -class RR_OT_sqe_create_review_session(bpy.types.Operator): - """ - Review a sequence of shots or a single shot. - Review shot will load all available preview sequences (.jpg / .png) of each found rendering - in to the sequence editor of the specified shot. - Review sequence does it for each shot in that sequence. - If user enabled use_blender_kitsu in the addon preferences, - this operator will create a linked metadata strip for the loaded shot on the top most channel. - """ - - bl_idname = "rr.sqe_create_review_session" - bl_label = "Create Review Session" - bl_description = ( - "Imports all available renderings for the specified shot / sequence " - "in to the sequence editor" - ) - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - render_dir = context.scene.rr.render_dir_path - if not render_dir: - return False - return bool( - context.scene.rr.is_render_dir_valid - and opsdata.is_shot_dir(render_dir) - or opsdata.is_sequence_dir(render_dir) - ) - - def load_strip_from_img_seq(self, context, directory, idx: int, frame_start: int = 0): - try: - # Get best preview files sequence. - image_sequence = opsdata.get_best_preview_sequence(directory) - - except NoImageSequenceAvailableException: - # if no preview files available create an empty image strip - # this assumes that when a folder is there exr sequences are available to inspect - logger.warning("%s found no preview sequence", directory.name) - exr_files = opsdata.gather_files_by_suffix( - directory, output=list, search_suffixes=[".exr"] - ) - - if not exr_files: - logger.error("%s found no exr or preview sequence", directory.name) - return - - image_sequence = exr_files[0] - logger.info("%s found %i exr frames", directory.name, len(image_sequence)) - - else: - logger.info("%s found %i preview frames", directory.name, len(image_sequence)) - - finally: - # Get frame start. - frame_start = frame_start or int(image_sequence[0].stem) - - # Create new image strip. - strip = context.scene.sequence_editor.sequences.new_image( - name=directory.name, - filepath=image_sequence[0].as_posix(), - channel=idx + 1, - frame_start=frame_start, - fit_method='ORIGINAL', - ) - - # Extend strip elements with all the available frames. - for f in image_sequence[1:]: - strip.elements.append(f.name) - - # Set strip properties. - strip.mute = True if image_sequence[0].suffix == ".exr" else False - - return strip - - def execute(self, context: bpy.types.Context) -> Set[str]: - # Clear existing strips and markers - context.scene.sequence_editor_clear() - context.scene.timeline_markers.clear() - addon_prefs = prefs.addon_prefs_get(context) - - render_dir = Path(context.scene.rr.render_dir_path) - shot_version_folders_dict = self.get_shot_folder_dict( - render_dir=render_dir, - max_versions_per_shot=addon_prefs.versions_max_count, - ) - - prev_frame_end: int = 1 - for shot_task_name, shot_version_folders in shot_version_folders_dict.items(): - shot_name = shot_task_name.split("-")[0] - logger.info("Loading versions of shot %s", shot_name) - imported_strips = self.import_shot_versions_as_strips( - context, - shot_version_folders, - frame_start=prev_frame_end, - shot_name=shot_name, - ) - - if not imported_strips: - continue - - # Query the strip that is the longest for metadata strip and prev_frame_end. - imported_strips.sort(key=lambda s: s.frame_final_duration) - strip_longest = imported_strips[-1] - prev_frame_end = strip_longest.frame_final_end - - if addon_prefs.skip_incomplete_renders: - for strip in imported_strips: - if strip.frame_final_duration < strip_longest.frame_final_duration: - context.scene.sequence_editor.sequences.remove(strip) - - # Perform kitsu operations if enabled. - if prefs.session_auth(context) and imported_strips: - if opsdata.is_active_project(): - sequence_name = shot_version_folders[0].parent.parent.parent.name - - # Create metadata strip. - metadata_strip = seq_opsdata.create_metadata_strip( - context.scene, - f"{strip_longest.name}_metadata-strip", - strip_longest.channel + 1, - strip_longest.frame_final_start, - strip_longest.frame_final_end, - ) - - logger.info( - "%s created Metadata Strip: %s", - strip_longest.name, - metadata_strip.name, - ) - - # Link metadata strip. - opsdata.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) - - else: - logger.error( - "Unable to perform kitsu operations. No active project or not authorized" - ) - - # Set default scene resolution to resolution of loaded image. - render_resolution_x = vars.RESOLUTION[0] - render_resolution_y = vars.RESOLUTION[1] - project = cache.project_active_get() - # If Kitsu add-on is enabled, fetch the resolution from the online project - if project: - # TODO: make the resolution fetching a bit more robust - # Assume resolution is a string 'x' - resolution = project.resolution.split('x') - render_resolution_x = int(resolution[0]) - render_resolution_y = int(resolution[1]) - - context.scene.render.resolution_x = render_resolution_x - context.scene.render.resolution_y = render_resolution_y - - # Change frame range and frame start. - opsdata.fit_frame_range_to_strips(context) - context.scene.frame_current = context.scene.frame_start - - # Setup color management. - opsdata.setup_color_management(context) - - # scan for approved renders, will modify strip.rr.is_approved prop - # which controls the custom gpu overlay - opsdata.update_sequence_statuses(context) - - bpy.ops.sequencer.select_all(action='DESELECT') - - util.redraw_ui() - - self.report( - {"INFO"}, - f"Imported {len(imported_strips)} Render Sequences", - ) - - return {"FINISHED"} - - def get_shot_folder_dict( - self, - render_dir: str, - max_versions_per_shot=32, - ) -> OrderedDict[str, List[Path]]: - shot_version_folders: List[Path] = [] - - # If render is sequence folder user wants to review whole sequence. - if opsdata.is_sequence_dir(render_dir): - for shot_dir in render_dir.iterdir(): - # TODO: Handle case when directory is empty - shot_version_folders.extend(list(shot_dir.iterdir())) - else: - # TODO: Handle case when directory is empty - shot_version_folders.extend(list(render_dir.iterdir())) - - shot_version_folders_dict = dict() - for shot_main_folder in shot_version_folders: - shot_name = opsdata.get_shot_name_from_dir(shot_main_folder) - for shot_folder in shot_main_folder.iterdir(): - if shot_name not in shot_version_folders_dict: - shot_version_folders_dict[shot_name] = [shot_folder] - else: - shot_version_folders_dict[shot_name].append(shot_folder) - - # Sort versions by date - for shot_name, shot_folder in shot_version_folders_dict.items(): - shot_version_folders_dict[shot_name] = sorted(shot_folder, reverse=False) - # Limit list to max number of versions - shot_version_folders_dict[shot_name] = shot_version_folders_dict[shot_name][ - -max_versions_per_shot: - ] - - # Sort shots by name - sorted_dict = OrderedDict(sorted(shot_version_folders_dict.items())) - - return sorted_dict - - def import_shot_versions_as_strips( - self, - context: bpy.types.Context, - shot_version_folders: List[Path], - frame_start: int, - shot_name: str, - ) -> List[bpy.types.Sequence]: - addon_prefs = prefs.addon_prefs_get(context) - imported_strips: bpy.types.Sequence = [] - - shots_folder_to_add = [] - - if addon_prefs.shot_name_filter == "": - shots_folder_to_add = shot_version_folders - else: - for shot_folder in shot_version_folders: - if addon_prefs.shot_name_filter in shot_folder.parent.name: - shots_folder_to_add.append(shot_folder) - - for idx, shot_folder in enumerate(shots_folder_to_add): - logger.info("Processing %s", shot_folder.name) - - use_video = addon_prefs.use_video and not ( - shot_folder != shot_version_folders[-1] and addon_prefs.use_video_latest_only - ) - - shot_strip = self.import_shot_as_strip( - context, - frame_start=frame_start, - channel_idx=idx, - shot_folder=shot_folder, - shot_name=shot_name, - use_video=use_video, - ) - imported_strips.append(shot_strip) - return imported_strips - - def import_shot_as_strip( - self, - context: bpy.types.Context, - frame_start: int, - channel_idx: int, - shot_folder: Path, - shot_name: str, - use_video=False, - ) -> bpy.types.Sequence: - context.scene.timeline_markers.new(shot_name, frame=frame_start) - - # Init sequencer - if not context.scene.sequence_editor: - context.scene.sequence_editor_create() - - ### Load preview sequences in vse. - - # Compose frames found text. - frames_found_text = opsdata.gen_frames_found_text(shot_folder) - - if use_video: - video_path = opsdata.get_farm_output_mp4_path_from_folder(shot_folder) - if not video_path: - logger.warning("%s found no .mp4 preview sequence", shot_folder.name) - video_path = shot_folder - strip = context.scene.sequence_editor.sequences.new_movie( - name=shot_folder.name, - filepath=video_path.as_posix(), - channel=channel_idx + 1, - frame_start=frame_start, - fit_method='ORIGINAL', - ) - else: - strip = self.load_strip_from_img_seq(context, shot_folder, channel_idx, frame_start) - - shot_datetime = datetime.fromtimestamp(shot_folder.stat().st_mtime) - time_str = shot_datetime.strftime("%B %d, %I:%M") - strip.name = f"{shot_folder.parent.name} ({time_str})" - strip.rr.shot_name = shot_name - strip.rr.is_render = True - strip.rr.frames_found_text = frames_found_text - - return strip - - -class RR_OT_setup_review_workspace(bpy.types.Operator): - """ - Makes Video Editing Workspace active and deletes all other workspaces. - Replaces File Browser area with Image Editor. - """ - - bl_idname = "rr.setup_review_workspace" - bl_label = "Setup Review Workspace" - bl_description = ( - "Makes Video Editing Workspace active and deletes all other workspaces. " - "Replaces File Browser area with Image Editor" - ) - bl_options = {"REGISTER", "UNDO"} - - def sequences_enum_items(self, context): - return [("None", "None", "None")] + cache.get_sequences_enum_list(self, context) - - sequence: bpy.props.EnumProperty( - name="Sequence", - description="Select which sequence to review", - items=sequences_enum_items, - ) - - @staticmethod - def delayed_setup_review_workspace(): - """This function can be used as a bpy.app.timer. - It is necessary to delay certain UI changing operations that rely - on previous UI changing operations, because Blender.""" - - context = bpy.context - - for window in context.window_manager.windows: - screen = window.screen - - for area in screen.areas: - # Change video editing workspace media browser to image editor. - if area.type == "FILE_BROWSER": - area.type = "IMAGE_EDITOR" - - # Disable filepath overlay on the strips in the VSE. - if area.spaces.active.type == 'SEQUENCE_EDITOR': - area.spaces.active.timeline_overlay.show_strip_source = False - area.spaces.active.timeline_overlay.show_strip_duration = False - - if area.spaces.active.view_type == 'PREVIEW': - area.spaces.active.show_overlays = False - - def invoke(self, context, _event): - - if not cache.project_active_get(): - return self.execute(context) - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - addon_prefs = prefs.addon_prefs_get(context) - - layout.prop(self, 'sequence') - if self.sequence != 'None': - layout.row().prop(addon_prefs, 'skip_incomplete_renders') - row = layout.row() - row.prop(addon_prefs, 'use_video') - if addon_prefs.use_video: - row.prop(addon_prefs, 'use_video_latest_only') - - layout.prop(addon_prefs, 'shot_name_filter') - - def execute(self, context: bpy.types.Context) -> Set[str]: - render_dir = context.scene.rr.render_dir - - scripts_path = bpy.utils.script_paths(use_user=False)[0] - template_path = "/startup/bl_app_templates_system/Video_Editing/startup.blend" - ws_filepath = Path(scripts_path + template_path) - bpy.ops.workspace.append_activate( - idname="Video Editing", - filepath=ws_filepath.as_posix(), - ) - - # Pre-fill render directory with farm output shots directory. - addon_prefs = prefs.addon_prefs_get(bpy.context) - context.scene.rr.render_dir = addon_prefs.farm_output_dir + "/shots" - if not Path(render_dir).exists(): - self.report( - {"ERROR"}, - f"Farm Output Directory {render_dir} doesn't exist, check Add-On preferences", - ) - - return {"CANCELLED"} - - # Init sqe. - if not context.scene.sequence_editor: - context.scene.sequence_editor_create() - - # Setup color management. - opsdata.setup_color_management(bpy.context) - - self.report({"INFO"}, "Setup Render Review Workspace") - - if self.sequence and self.sequence != 'None': - cache.sequence_active_set_by_id(context, self.sequence) - context.scene.rr.render_dir += "/" + cache.sequence_active_get().name - bpy.ops.rr.sqe_create_review_session() - - # Switch File Browser to Image Editor (needs to be done with a delay). - bpy.app.timers.register(self.delayed_setup_review_workspace, first_interval=1) - - return {"FINISHED"} - - -class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): - bl_idname = "rr.sqe_inspect_exr_sequence" - bl_label = "Inspect EXR" - bl_description = "Loads EXR sequence for selected sequence strip in image editor, if it exists" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - active_strip = context.scene.sequence_editor.active_strip - image_editor = opsdata.get_image_editor(context) - - if not active_strip: - cls.poll_message_set("No sequence strip selected") - return False - - if not active_strip.rr.is_render: - cls.poll_message_set("Selected sequence strip is not an imported render") - return False - - if not image_editor: - cls.poll_message_set("No image editor open in the current workspace") - return False - - output_dir = opsdata.get_strip_folder(active_strip) - # Find exr sequence. - for f in output_dir.iterdir(): - if f.is_file() and f.suffix == ".exr": - return True - - cls.poll_message_set("Selected strip is not EXR sequence") - return False - - def execute(self, context: bpy.types.Context) -> Set[str]: - active_strip = context.scene.sequence_editor.active_strip - image_editor = opsdata.get_image_editor(context) - output_dir = opsdata.get_strip_folder(active_strip) - - # Find exr sequence. - exr_seq = [f for f in output_dir.iterdir() if f.is_file() and f.suffix == ".exr"] - - exr_seq.sort(key=lambda p: p.name) - exr_seq_frame_start = int(exr_seq[0].stem) - offset = exr_seq_frame_start - active_strip.frame_final_start - - # Remove all images with same filepath that are already loaded. - img_to_rm: bpy.types.Image = [] - for img in bpy.data.images: - if Path(bpy.path.abspath(img.filepath)) == exr_seq[0]: - img_to_rm.append(img) - - for img in img_to_rm: - bpy.data.images.remove(img) - - if bpy.app.version_string.split('.')[0] == '3': - color_space_name = "Linear" - else: - color_space_name = "Linear Rec.709" - - # Create new image datablock. - image = bpy.data.images.load(exr_seq[0].as_posix(), check_existing=True) - image.name = exr_seq[0].parent.name + "_RENDER" - image.source = "SEQUENCE" - image.colorspace_settings.name = color_space_name - - # Set active image. - image_editor.spaces.active.image = image - image_editor.spaces.active.image_user.frame_duration = 5000 - image_editor.spaces.active.image_user.frame_offset = offset - - return {"FINISHED"} - - -class RR_OT_sqe_clear_exr_inspect(bpy.types.Operator): - bl_idname = "rr.sqe_clear_exr_inspect" - bl_label = "Clear EXR Inspect" - bl_description = "Removes the active image from the image editor" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - image_editor = cls._get_image_editor(context) - if not image_editor: - cls.poll_message_set("No image editor open in the current workspace") - return False - if not image_editor.spaces.active.image: - cls.poll_message_set("No image to clear from image editor") - return False - return True - - def execute(self, context: bpy.types.Context) -> Set[str]: - image_editor = self._get_image_editor(context) - image_editor.spaces.active.image = None - return {"FINISHED"} - - @classmethod - def _get_image_editor(self, context: bpy.types.Context) -> Optional[bpy.types.Area]: - image_editor = None - - for area in bpy.context.screen.areas: - if area.type == "IMAGE_EDITOR": - image_editor = area - - return image_editor - - -class RR_OT_sqe_approve_render(bpy.types.Operator): - bl_idname = "rr.sqe_approve_render" - bl_label = "Push To Edit & Approve Render" - bl_description = ( - "Copies the selected strip render from the farm_output to the shot_frames directory. " - "Existing render in shot_frames will be renamed for extra backup" - ) - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - active_strip = context.scene.sequence_editor.active_strip - addon_prefs = prefs.addon_prefs_get(bpy.context) - - if not addon_prefs.shot_playblast_root_dir: - cls.poll_message_set("Playblast directory not set") - return False - - if not active_strip: - cls.poll_message_set("No sequence strip selected") - return False - - if not active_strip.rr.is_render: - cls.poll_message_set("Selected sequence strip is not an imported render") - return False - - if active_strip.rr.is_approved: - cls.poll_message_set("Selected sequence strip is already approved") - return False - - return True - - def execute(self, context: bpy.types.Context) -> Set[str]: - active_strip = context.scene.sequence_editor.active_strip - - if not active_strip.rr.is_pushed_to_edit: - bpy.ops.rr.sqe_push_to_edit() - - strip_dir = opsdata.get_strip_folder(active_strip) - frames_root_dir = opsdata.get_frames_root_dir(active_strip) - shot_frames_backup_path = opsdata.get_shot_frames_backup_path(active_strip) - metadata_path = opsdata.get_shot_frames_metadata_path(active_strip) - - # Create Shot Frames path if not exists yet. - if frames_root_dir.exists(): - # Delete backup if exists. - if shot_frames_backup_path.exists(): - shutil.rmtree(shot_frames_backup_path) - - # Rename current to backup. - frames_root_dir.rename(shot_frames_backup_path) - logger.info( - "Created backup: %s > %s", - frames_root_dir.name, - shot_frames_backup_path.name, - ) - else: - frames_root_dir.mkdir(parents=True) - logger.info("Created dir in Shot Frames: %s", frames_root_dir.as_posix()) - - # Copy dir. - opsdata.copytree_verbose( - strip_dir, - frames_root_dir, - dirs_exist_ok=True, - ) - logger.info("Copied: %s \nTo: %s", strip_dir.as_posix(), frames_root_dir.as_posix()) - - # Update metadata json. - if not metadata_path.exists(): - metadata_path.touch() - opsdata.save_to_json( - {"source_current": strip_dir.as_posix(), "source_backup": ""}, - metadata_path, - ) - logger.info("Created metadata.json: %s", metadata_path.as_posix()) - else: - json_dict = opsdata.load_json(metadata_path) - # Source backup will get value from old source current. - json_dict["source_backup"] = json_dict["source_current"] - # Source current will get value from strip dir. - json_dict["source_current"] = strip_dir.as_posix() - - opsdata.save_to_json(json_dict, metadata_path) - - # Scan for approved renders. - opsdata.update_sequence_statuses(context) - util.redraw_ui() - - # Log. - self.report({"INFO"}, f"Updated {frames_root_dir.name} in Shot Frames") - logger.info("Updated metadata in: %s", metadata_path.as_posix()) - - return {"FINISHED"} - - def invoke(self, context, event): - active_strip = context.scene.sequence_editor.active_strip - frames_root_dir = opsdata.get_frames_root_dir(active_strip) - width = 200 + len(frames_root_dir.as_posix()) * 5 - return context.window_manager.invoke_props_dialog(self, width=width) - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - active_strip = context.scene.sequence_editor.active_strip - strip_dir = opsdata.get_strip_folder(active_strip) - frames_root_dir = opsdata.get_frames_root_dir(active_strip) - - layout.separator() - layout.row(align=True).label(text="From Farm Output:", icon="RENDER_ANIMATION") - layout.row(align=True).label(text=strip_dir.as_posix()) - - layout.separator() - layout.row(align=True).label(text="To Shot Frames:", icon="FILE_TICK") - layout.row(align=True).label(text=frames_root_dir.as_posix()) - - layout.separator() - layout.row(align=True).label(text="Update Shot Frames?") - - -class RR_OT_sqe_update_sequence_statuses(bpy.types.Operator): - bl_idname = "rr.update_sequence_statuses" - bl_label = "Update Sequence Statuses" - bl_description = ( - "Scans sequence editor and updates flags for which ones are pushed " - "to the edit and which one is the currently approved version " - "by reading the metadata.json files" - ) - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - return bool(context.scene.sequence_editor.sequences_all) - - def execute(self, context: bpy.types.Context) -> Set[str]: - approved_strips = opsdata.update_sequence_statuses(context)[0] - - if approved_strips: - self.report( - {"INFO"}, - f"Found approved {'render' if len(approved_strips) == 1 else 'renders'}: {', '.join(s.name for s in approved_strips)}", - ) - else: - self.report({"INFO"}, "Found no approved renders") - return {"FINISHED"} - - -class RR_OT_open_path(bpy.types.Operator): - """ - Opens cls.filepath in explorer. Supported for win / mac / linux. - """ - - bl_idname = "rr.open_path" - bl_label = "Open Path" - bl_description = "Opens filepath in system default file browser" - - filepath: bpy.props.StringProperty( # type: ignore - name="Filepath", - description="Filepath that will be opened in explorer", - default="", - ) - - def execute(self, context: bpy.types.Context) -> Set[str]: - if not self.filepath: - self.report({"ERROR"}, "Can't open empty path in explorer") - return {"CANCELLED"} - - filepath = Path(self.filepath) - if filepath.is_file(): - filepath = filepath.parent - - if not filepath.exists(): - filepath = self._find_latest_existing_folder(filepath) - - if sys.platform == "darwin": - subprocess.check_call(["open", filepath.as_posix()]) - - elif sys.platform == "linux2" or sys.platform == "linux": - subprocess.check_call(["xdg-open", filepath.as_posix()]) - - elif sys.platform == "win32": - os.startfile(filepath.as_posix()) - - else: - self.report({"ERROR"}, f"Can't open explorer. Unsupported platform {sys.platform}") - return {"CANCELLED"} - - return {"FINISHED"} - - def _find_latest_existing_folder(self, path: Path) -> Path: - if path.exists() and path.is_dir(): - return path - else: - return self._find_latest_existing_folder(path.parent) - - -class RR_OT_sqe_isolate_strip_exit(bpy.types.Operator): - bl_idname = "rr.sqe_isolate_strip_exit" - bl_label = "Exit isolate strip view" - bl_description = "Exits isolate strip view and restores previous state" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context: bpy.types.Context) -> Set[str]: - for i in context.scene.rr.isolate_view: - try: - strip = context.scene.sequence_editor.sequences[i.name] - except KeyError: - logger.error("Exit isolate view: Strip does not exist %s", i.name) - continue - - strip.mute = i.mute - - # Clear all items. - context.scene.rr.isolate_view.clear() - - return {"FINISHED"} - - -class RR_OT_sqe_isolate_strip_enter(bpy.types.Operator): - bl_idname = "rr.sqe_isolate_strip_enter" - bl_label = "Isolate Strip" - bl_description = "Isolate all selected sequence strips, others are hidden. Previous state is saved and restorable" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - active_strip = context.scene.sequence_editor.active_strip - return bool(active_strip) - - def execute(self, context: bpy.types.Context) -> Set[str]: - sequences = list(context.scene.sequence_editor.sequences_all) - - if context.scene.rr.isolate_view.items(): - bpy.ops.rr.sqe_isolate_strip_exit() - - # Mute all and save state to restore later. - for s in sequences: - # Save this state to restore it later. - item = context.scene.rr.isolate_view.add() - item.name = s.name - item.mute = s.mute - s.mute = True - - # Unmute selected. - for s in context.selected_sequences: - s.mute = False - - return {"FINISHED"} - - -class RR_OT_sqe_push_to_edit(bpy.types.Operator): - """ - This operator pushes the active render strip to the edit. Only .mp4 files will be pushed to edit. - If the .mp4 file is not existent but the preview .jpg sequence is in the render folder. This operator - creates an .mp4 with ffmpeg. The .mp4 file will be named after the flamenco naming convention, but when - copied over to the Shot Previews it will be renamed and gets a version string. - """ - - bl_idname = "rr.sqe_push_to_edit" - bl_label = "Push To Edit" - bl_description = ( - "Copies .mp4 file of current sequence strip to the shot preview directory with " - "auto version incrementation. " - "Creates .mp4 with ffmpeg if not existent yet" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - addon_prefs = prefs.addon_prefs_get(context) - active_strip = context.scene.sequence_editor.active_strip - - if not addon_prefs.shot_playblast_root_dir: - cls.poll_message_set("No shot playblast root dir set") - return False - - if not active_strip: - cls.poll_message_set("No active strip") - return False - - if not active_strip.rr.is_render: - cls.poll_message_set("Selected sequence strip is not an imported render") - return False - - if not active_strip.rr.is_pushed_to_edit: - cls.poll_message_set("Selected sequence strip is already pushed to edit") - return False - return True - - def execute(self, context: bpy.types.Context) -> Set[str]: - active_strip = context.scene.sequence_editor.active_strip - - render_dir = opsdata.get_strip_folder(active_strip) - shot_previews_dir = opsdata.get_shot_previews_path(active_strip) - metadata_path = shot_previews_dir / "metadata.json" - - # -------------GET MP4 OR CREATE WITH FFMPEG --------------- - # Trying to get render_mp4_path will throw error if no jpg files are available. - try: - mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) - except NoImageSequenceAvailableException: - # No jpeg files available. - self.report({"ERROR"}, f"No preview files available in {render_dir.as_posix()}") - return {"CANCELLED"} - - # If mp4 path does not exist, use ffmpeg to create preview file. - if not mp4_path.exists(): - preview_files = opsdata.get_best_preview_sequence(render_dir) - fffmpeg_command = f"ffmpeg -start_number {int(preview_files[0].stem)} -framerate {vars.FPS} -i {render_dir.as_posix()}/%06d{preview_files[0].suffix} -c:v libx264 -preset medium -crf 23 -pix_fmt yuv420p {mp4_path.as_posix()}" - logger.info("Creating .mp4 with ffmpeg") - subprocess.call(fffmpeg_command, shell=True) - logger.info("Created .mp4: %s", mp4_path.as_posix()) - else: - logger.info("Found existing .mp4 file: %s", mp4_path.as_posix()) - - # --------------COPY MP4 TO Shot Previews ----------------. - - # Create edit path if not exists yet. - if not shot_previews_dir.exists(): - shot_previews_dir.mkdir(parents=True) - logger.info("Created dir in Shot Previews: %s", shot_previews_dir.as_posix()) - - # Get edit_filepath. - edit_filepath = self.get_edit_filepath(active_strip) - - # Copy mp4 to edit filepath. - shutil.copy2(mp4_path.as_posix(), edit_filepath.as_posix()) - logger.info("Copied: %s \nTo: %s", mp4_path.as_posix(), edit_filepath.as_posix()) - - # ----------------UPDATE METADATA.JSON ------------------. - - # Create metadata json. - if not metadata_path.exists(): - metadata_path.touch() - logger.info("Created metadata.json: %s", metadata_path.as_posix()) - opsdata.save_to_json({}, metadata_path) - - # Udpate metadata json. - json_obj = opsdata.load_json(metadata_path) - json_obj[edit_filepath.name] = mp4_path.as_posix() - opsdata.save_to_json( - json_obj, - metadata_path, - ) - logger.info("Updated metadata in: %s", metadata_path.as_posix()) - - # Scan for approved renders. - opsdata.update_sequence_statuses(context) - - # Log. - self.report( - {"INFO"}, - f"Pushed to edit: {edit_filepath.as_posix()}", - ) - return {"FINISHED"} - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - active_strip = context.scene.sequence_editor.active_strip - edit_filepath = self.get_edit_filepath(active_strip) - - try: - mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) - except NoImageSequenceAvailableException: - layout.separator() - layout.row(align=True).label(text="No preview files available", icon="ERROR") - return - - text = "From Farm Output:" - if not mp4_path.exists(): - text = "From Farm Output (will be created with ffmpeg):" - - layout.separator() - layout.row(align=True).label(text=text, icon="RENDER_ANIMATION") - layout.row(align=True).label(text=mp4_path.as_posix()) - - layout.separator() - layout.row(align=True).label(text="To Shot Previews:", icon="FILE_TICK") - layout.row(align=True).label(text=edit_filepath.as_posix()) - - layout.separator() - layout.row(align=True).label(text="Copy to Shot Previews?") - - def invoke(self, context, event): - active_strip = context.scene.sequence_editor.active_strip - try: - mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) - except NoImageSequenceAvailableException: - width = 200 - else: - width = 200 + len(mp4_path.as_posix()) * 5 - return context.window_manager.invoke_props_dialog(self, width=width) - - def get_edit_filepath(self, strip: bpy.types.Sequence) -> Path: - delimiter = vars.DELIMITER - render_dir = opsdata.get_strip_folder(strip) - shot_previews_dir = opsdata.get_shot_previews_path(strip) - - # Find latest edit version. - existing_files: List[Path] = [] - increment = "v001" - if shot_previews_dir.exists(): - for file in shot_previews_dir.iterdir(): - if not file.is_file(): - continue - - if not file.name.startswith(opsdata.get_shot_dot_task_type(render_dir)): - continue - - version = util.get_version(file.name) - if not version: - continue - - if ( - file.name.replace(version, "") - == f"{opsdata.get_shot_dot_task_type(render_dir)}{delimiter}.mp4" - ): - existing_files.append(file) - - existing_files.sort(key=lambda f: f.name) - - # Get version string. - if len(existing_files) > 0: - latest_version = util.get_version(existing_files[-1].name) - increment = "v{:03}".format(int(latest_version.replace("v", "")) + 1) - - # Compose edit filepath of new mp4 file. - edit_filepath = ( - shot_previews_dir - / f"{opsdata.get_shot_dot_task_type(render_dir)}{delimiter}{increment}.mp4" - ) - return edit_filepath - - -# ----------------REGISTER--------------. - - -classes = [ - RR_OT_sqe_create_review_session, - RR_OT_setup_review_workspace, - RR_OT_sqe_inspect_exr_sequence, - RR_OT_sqe_clear_exr_inspect, - RR_OT_sqe_approve_render, - RR_OT_sqe_update_sequence_statuses, - RR_OT_open_path, - RR_OT_sqe_isolate_strip_enter, - RR_OT_sqe_isolate_strip_exit, - RR_OT_sqe_push_to_edit, -] - -addon_keymap_items = [] - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - # register hotkeys - # does not work if blender runs in background - if not bpy.app.background: - global addon_keymap_items - keymap = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name="Window") - - # Isolate strip. - addon_keymap_items.append( - keymap.keymap_items.new("rr.sqe_isolate_strip_enter", value="PRESS", type="ONE") - ) - - # Umute all. - addon_keymap_items.append( - keymap.keymap_items.new( - "rr.sqe_isolate_strip_exit", value="PRESS", type="ONE", alt=True - ) - ) - for kmi in addon_keymap_items: - logger.info("Registered new hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - - # Does not work if blender runs in background. - if not bpy.app.background: - global addon_keymap_items - # Remove hotkeys. - keymap = bpy.context.window_manager.keyconfigs.addon.keymaps["Window"] - for kmi in addon_keymap_items: - logger.info("Remove hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name) - keymap.keymap_items.remove(kmi) - - addon_keymap_items.clear() diff --git a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py deleted file mode 100644 index 231b77d8..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py +++ /dev/null @@ -1,431 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import json -import shutil -from pathlib import Path -from typing import Set, Union, Optional, List, Dict, Any, Tuple - -import bpy - -from . import vars, checksqe, util -from .. import prefs, cache -from ..sqe import opsdata as sqe_opsdata -from .exception import NoImageSequenceAvailableException - -from ..logger import LoggerFactory - -logger = LoggerFactory.getLogger() - - -copytree_list: List[Path] = [] -copytree_num_of_items: int = 0 - - -def copytree_verbose(src: Union[str, Path], dest: Union[str, Path], **kwargs): - _copytree_init_progress_update(Path(src)) - shutil.copytree(src, dest, copy_function=_copy2_tree_progress, **kwargs) - _copytree_clear_progress_update() - - -def _copytree_init_progress_update(source_dir: Path): - global copytree_num_of_items - file_list = [f for f in source_dir.glob("**/*") if f.is_file()] - copytree_num_of_items = len(file_list) - - -def _copy2_tree_progress(src, dst): - """ - Function that can be used for copy_function - argument on shutil.copytree function. - Logs every item that is currently copied. - """ - global copytree_num_of_items - global copytree_list - - copytree_list.append(Path(src)) - progress = round((len(copytree_list) * 100) / copytree_num_of_items) - logger.info("Copying %s (%i%%)", src, progress) - shutil.copy2(src, dst) - - -def _copytree_clear_progress_update(): - global copytree_num_of_items - - copytree_num_of_items = 0 - copytree_list.clear() - - -def get_valid_cs_sequences( - context: bpy.types.Context, sequence_list: List[bpy.types.Sequence] = [] -) -> List[bpy.types.Sequence]: - - sequences: List[bpy.types.Sequence] = [] - - if sequence_list: - sequences = sequence_list - else: - sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all - - if cache.project_active_get(): - - valid_sequences = [ - s - for s in sequences - if s.type in ["MOVIE", "IMAGE"] and not s.mute and not s.kitsu.initialized - ] - else: - valid_sequences = [s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute] - - return valid_sequences - - -def get_frames_root_dir(strip: bpy.types.Sequence) -> Path: - # sf = shot_frames | fo = farm_output. - addon_prefs = prefs.addon_prefs_get(bpy.context) - fo_dir = get_strip_folder(strip) - sf_dir = addon_prefs.frames_root_dir / fo_dir.parent.relative_to(fo_dir.parents[3]) - - return sf_dir - - -def get_strip_folder(strip: bpy.types.Sequence) -> Path: - if hasattr(strip, 'directory'): - return Path(strip.directory) - else: - return Path(strip.filepath).parent - - -def get_shot_previews_path(strip: bpy.types.Sequence) -> Path: - # Fo > farm_output. - addon_prefs = prefs.addon_prefs_get(bpy.context) - fo_dir = get_strip_folder(strip) - shot_previews_dir = addon_prefs.shot_playblast_root_dir / fo_dir.parent.relative_to( - fo_dir.parents[3] - ) - - return shot_previews_dir - - -def get_shot_dot_task_type(path: Path): - return path.parent.name - - -def get_farm_output_mp4_path(strip: bpy.types.Sequence) -> Path: - render_dir = get_strip_folder(strip) - return get_farm_output_mp4_path_from_folder(render_dir) - - -def get_farm_output_mp4_path_from_folder(render_dir: str) -> Path: - render_dir = Path(render_dir) - shot_name = render_dir.parent.name - - # 070_0040_A.lighting-101-136.mp4 #farm always does .lighting not .comp - # because flamenco writes in and out frame in filename we need check the first and - # last frame in the folder - preview_seq = get_best_preview_sequence(render_dir) - - mp4_filename = f"{shot_name}-{int(preview_seq[0].stem)}-{int(preview_seq[-1].stem)}.mp4" - - return render_dir / mp4_filename - - -def get_best_preview_sequence(dir: Path) -> List[Path]: - - files: List[List[Path]] = gather_files_by_suffix( - dir, output=dict, search_suffixes=[".jpg", ".png"] - ) - if not files: - raise NoImageSequenceAvailableException(f"No preview files found in: {dir.as_posix()}") - - # Select the right images sequence. - if len(files) == 1: - # If only one image sequence available take that. - preview_seq = files[list(files.keys())[0]] - - # Both jpg and png available. - else: - # If same amount of frames take png. - if len(files[".jpg"]) == len(files[".png"]): - preview_seq = files[".png"] - else: - # If not, take whichever is longest. - preview_seq = [files[".jpg"], files[".png"]].sort(key=lambda x: len(x))[-1] - - return preview_seq - - -def get_shot_frames_backup_path(strip: bpy.types.Sequence) -> Path: - fs_dir = get_frames_root_dir(strip) - return fs_dir.parent / f"_backup.{fs_dir.name}" - - -def get_shot_frames_metadata_path(strip: bpy.types.Sequence) -> Path: - fs_dir = get_frames_root_dir(strip) - return fs_dir.parent / "metadata.json" - - -def get_shot_previews_metadata_path(strip: bpy.types.Sequence) -> Path: - fs_dir = get_shot_previews_path(strip) - return fs_dir / "metadata.json" - - -def load_json(path: Path) -> Any: - with open(path.as_posix(), "r") as file: - obj = json.load(file) - return obj - - -def save_to_json(obj: Any, path: Path) -> None: - with open(path.as_posix(), "w") as file: - json.dump(obj, file, indent=4) - - -def update_sequence_statuses( - context: bpy.types.Context, -) -> List[bpy.types.Sequence]: - return update_is_approved(context), update_is_pushed_to_edit(context) - - -def update_is_approved( - context: bpy.types.Context, -) -> List[bpy.types.Sequence]: - sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] - - approved_strips = [] - - for s in sequences: - metadata_path = get_shot_frames_metadata_path(s) - if not metadata_path.exists(): - continue - json_obj = load_json(metadata_path) # TODO: prevent opening same json multi times - - if Path(json_obj["source_current"]) == get_strip_folder(s): - s.rr.is_approved = True - approved_strips.append(s) - logger.info("Detected approved strip: %s", s.name) - else: - s.rr.is_approved = False - - return approved_strips - - -def update_is_pushed_to_edit( - context: bpy.types.Context, -) -> List[bpy.types.Sequence]: - sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] - - pushed_strips = [] - - for s in sequences: - metadata_path = get_shot_previews_metadata_path(s) - if not metadata_path.exists(): - continue - - json_obj = load_json(metadata_path) - - valid_paths = {Path(value).parent for _key, value in json_obj.items()} - - if get_strip_folder(s) in valid_paths: - s.rr.is_pushed_to_edit = True - pushed_strips.append(s) - logger.info("Detected pushed strip: %s", s.name) - else: - s.rr.is_pushed_to_edit = False - - return pushed_strips - - -def gather_files_by_suffix( - dir: Path, output=str, search_suffixes: List[str] = [".jpg", ".png", ".exr"] -) -> Union[str, List, Dict]: - """ - Gathers files in dir that end with an extension in search_suffixes. - Supported values for output: str, list, dict - """ - - files: Dict[str, List[Path]] = {} - - # Gather files. - for f in dir.iterdir(): - if not f.is_file(): - continue - - for suffix in search_suffixes: - if f.suffix == suffix: - files.setdefault(suffix, []) - files[suffix].append(f) - - # Sort. - for suffix, file_list in files.items(): - files[suffix] = sorted(file_list, key=lambda f: f.name) - - # Return. - if output == str: - return_str = "" - for suffix, file_list in files.items(): - return_str += f" | {suffix}: {len(file_list)}" - - # Replace first occurence, we dont want that at the beginning. - return_str = return_str.replace(" | ", "", 1) - - return return_str - - elif output == dict: - return files - - elif output == list: - output_list = [] - for suffix, file_list in files.items(): - output_list.append(file_list) - - return output_list - else: - raise ValueError( - f"Supported output types are: str, dict, list. {str(output)} not implemented yet." - ) - - -def gen_frames_found_text(dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"]) -> str: - files_dict = gather_files_by_suffix(dir, output=dict, search_suffixes=search_suffixes) - - frames_found_text = "" # frames found text will be used in ui - for suffix, file_list in files_dict.items(): - frames_found_text += f" | {suffix}: {len(file_list)}" - - # Replace first occurence, we dont want that at the beginning. - frames_found_text = frames_found_text.replace( - " | ", - "", - 1, - ) - return frames_found_text - - -def is_sequence_dir(dir: Path) -> bool: - return dir.parent.name == "shots" - - -def is_shot_dir(dir: Path) -> bool: - return dir.parent.parent.name == "shots" - - -def get_shot_name_from_dir(dir: Path) -> str: - return dir.stem # 060_0010_A.lighting > 060_0010_A - - -def get_image_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]: - image_editor = None - - for area in context.screen.areas: - if area.type == "IMAGE_EDITOR": - image_editor = area - - return image_editor - - -def get_sqe_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]: - sqe_editor = None - - for area in context.screen.areas: - if area.type == "SEQUENCE_EDITOR": - sqe_editor = area - - return sqe_editor - - -def fit_frame_range_to_strips( - context: bpy.types.Context, strips: Optional[List[bpy.types.Sequence]] = None -) -> Tuple[int, int]: - def get_sort_tuple(strip: bpy.types.Sequence) -> Tuple[int, int]: - return (strip.frame_final_start, strip.frame_final_duration) - - if not strips: - strips = context.scene.sequence_editor.sequences_all - - if not strips: - return (0, 0) - - strips = list(strips) - strips.sort(key=get_sort_tuple) - - context.scene.frame_start = strips[0].frame_final_start - context.scene.frame_end = strips[-1].frame_final_end - 1 - - return (context.scene.frame_start, context.scene.frame_end) - - -def get_top_level_valid_strips_continious( - context: bpy.types.Context, -) -> List[bpy.types.Sequence]: - - sequences_tmp = get_valid_cs_sequences( - context, sequence_list=list(context.scene.sequence_editor.sequences_all) - ) - - sequences_tmp.sort(key=lambda s: (s.channel, s.frame_final_start), reverse=True) - sequences: List[bpy.types.Sequence] = [] - - for strip in sequences_tmp: - - occ_ranges = checksqe.get_occupied_ranges_for_strips(sequences) - s_range = range(strip.frame_final_start, strip.frame_final_end + 1) - - if not checksqe.is_range_occupied(s_range, occ_ranges): - sequences.append(strip) - - return sequences - - -def setup_color_management(context: bpy.types.Context) -> None: - if context.scene.view_settings.view_transform != 'Standard': - context.scene.view_settings.view_transform = 'Standard' - logger.info("Set view transform to: Standard") - - -def is_active_project() -> bool: - return bool(cache.project_active_get()) - - -def link_strip_by_name( - context: bpy.types.Context, - strip: bpy.types.Sequence, - shot_name: str, - sequence_name: str, -) -> None: - # Get seq and shot. - active_project = cache.project_active_get() - seq = active_project.get_sequence_by_name(sequence_name) - shot = active_project.get_shot_by_name(seq, shot_name) - - if not shot: - logger.error("Unable to find shot %s on kitsu", shot_name) - return - - sqe_opsdata.link_metadata_strip(context, shot, seq, strip) - - # Log. - t = "Linked strip: %s to shot: %s with ID: %s" % ( - strip.name, - shot.name, - shot.id, - ) - logger.info(t) - util.redraw_ui() diff --git a/scripts-blender/addons/blender_kitsu/render_review/props.py b/scripts-blender/addons/blender_kitsu/render_review/props.py deleted file mode 100644 index a6be5b28..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/props.py +++ /dev/null @@ -1,102 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -from typing import Set, Union, Optional, List, Dict, Any -from pathlib import Path - -import bpy - - -from ..logger import LoggerFactory - -logger = LoggerFactory.getLogger() - - -class RR_isolate_collection_prop(bpy.types.PropertyGroup): - mute: bpy.props.BoolProperty() - - -class RR_property_group_scene(bpy.types.PropertyGroup): - """""" - - render_dir: bpy.props.StringProperty(name="Render Directory", subtype="DIR_PATH") - isolate_view: bpy.props.CollectionProperty(type=RR_isolate_collection_prop) - - @property - def render_dir_path(self): - if not self.is_render_dir_valid: - return None - return Path(bpy.path.abspath(self.render_dir)).absolute() - - @property - def is_render_dir_valid(self) -> bool: - if not self.render_dir: - return False - - if not bpy.data.filepath and self.render_dir.startswith("//"): - return False - - return True - - -class RR_property_group_sequence(bpy.types.PropertyGroup): - """ - Property group that will be registered on sequence strips. - """ - - is_render: bpy.props.BoolProperty(name="Is Render") - is_approved: bpy.props.BoolProperty(name="Is Approved") - is_pushed_to_edit: bpy.props.BoolProperty(name="Is Pushed To Edit") - frames_found_text: bpy.props.StringProperty(name="Frames Found") - shot_name: bpy.props.StringProperty(name="Shot") - - -# ----------------REGISTER--------------. - -classes = [ - RR_isolate_collection_prop, - RR_property_group_scene, - RR_property_group_sequence, -] - - -def register(): - - for cls in classes: - bpy.utils.register_class(cls) - - # Scene Properties. - bpy.types.Scene.rr = bpy.props.PointerProperty( - name="Render Review", - type=RR_property_group_scene, - description="Metadata that is required for render_review", - ) - - # Sequence Properties. - bpy.types.Sequence.rr = bpy.props.PointerProperty( - name="Render Review", - type=RR_property_group_sequence, - description="Metadata that is required for render_review", - ) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ui.py b/scripts-blender/addons/blender_kitsu/render_review/ui.py deleted file mode 100644 index 91c73e15..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/ui.py +++ /dev/null @@ -1,178 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -from pathlib import Path - -from typing import Set, Union, Optional, List, Dict, Any - -import bpy - -from .ops import ( - RR_OT_sqe_create_review_session, - RR_OT_setup_review_workspace, - RR_OT_sqe_inspect_exr_sequence, - RR_OT_sqe_clear_exr_inspect, - RR_OT_sqe_approve_render, - RR_OT_sqe_update_sequence_statuses, - RR_OT_open_path, - RR_OT_sqe_push_to_edit, -) -from . import opsdata -from .. import prefs - - -class RR_PT_render_review(bpy.types.Panel): - """ """ - - bl_category = "Render Review" - bl_label = "Render Review" - bl_space_type = "SEQUENCE_EDITOR" - bl_region_type = "UI" - bl_order = 10 - - def draw(self, context: bpy.types.Context) -> None: - - addon_prefs = prefs.addon_prefs_get(context) - - # Create box. - layout = self.layout - box = layout.box() - - # Label and setup workspace. - row = box.row(align=True) - row.label(text="Review", icon="CAMERA_DATA") - row.operator(RR_OT_setup_review_workspace.bl_idname, text="", icon="WINDOW") - - # Render dir prop. - row = box.row(align=True) - row.prop(context.scene.rr, "render_dir") - - # Create session. - render_dir = context.scene.rr.render_dir_path - text = f"Invalid Render Directory" - if render_dir: - if opsdata.is_sequence_dir(render_dir): - text = f"Review Sequence: {render_dir.name}" - elif opsdata.is_shot_dir(render_dir): - text = f"Review Shot: {render_dir.stem}" - - row = box.row(align=True) - row.operator(RR_OT_sqe_create_review_session.bl_idname, text=text, icon="PLAY") - row = box.row(align=True) - row.prop(addon_prefs, 'use_video') - if addon_prefs.use_video: - row.prop(addon_prefs, 'use_video_latest_only') - - # Warning if kitsu on but not logged in. - if not prefs.session_auth(context): - row = box.split(align=True, factor=0.7) - row.label(text="Kitsu enabled but not logged in", icon="ERROR") - row.operator("kitsu.session_start", text="Login") - - elif not opsdata.is_active_project(): - row = box.row(align=True) - row.label(text="Kitsu enabled but no active project", icon="ERROR") - - sqe = context.scene.sequence_editor - if not sqe: - return - active_strip = sqe.active_strip - if active_strip and active_strip.rr.is_render: - # Create box. - layout = self.layout - box = layout.box() - box.label(text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF") - box.separator() - - # Render dir name label and open file op. - row = box.row(align=True) - directory = opsdata.get_strip_folder(active_strip) - row.label(text=f"Folder: {directory.name}") - row.operator( - RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="", emboss=False - ).filepath = bpy.path.abspath(directory.as_posix()) - - # Nr of frames. - box.row(align=True).label(text=f"Frames: {active_strip.rr.frames_found_text}") - - # Inspect exr. - text = "Inspect EXR" - icon = "VIEWZOOM" - if not opsdata.get_image_editor(context): - text = "Inspect EXR: Needs Image Editor" - icon = "ERROR" - - row = box.row(align=True) - row.operator(RR_OT_sqe_inspect_exr_sequence.bl_idname, icon=icon, text=text) - row.operator(RR_OT_sqe_clear_exr_inspect.bl_idname, text="", icon="X") - - # Approve render & udpate approved. - row = box.row(align=True) - - text = "Push To Edit & Approve Render" - if active_strip.rr.is_pushed_to_edit: - text = "Approve Render" - row.operator(RR_OT_sqe_approve_render.bl_idname, icon="CHECKMARK", text=text) - row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH") - - # Push to edit. - if not addon_prefs.shot_playblast_root_dir: - shot_previews_dir = "" # ops handle invalid path - else: - shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix() - - row = box.row(align=True) - row.operator(RR_OT_sqe_push_to_edit.bl_idname, icon="EXPORT") - row.operator(RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="").filepath = ( - shot_previews_dir - ) - - # Push strip to Kitsu. - box.row().operator('kitsu.sqe_push_shot', icon='URL') - - -def RR_topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None: - layout = self.layout - op = layout.operator(RR_OT_setup_review_workspace.bl_idname, text="Render Review") - - -# ----------------REGISTER--------------. - -classes = [ - RR_PT_render_review, -] - - -def register(): - - for cls in classes: - bpy.utils.register_class(cls) - - # Append to topbar file new. - bpy.types.TOPBAR_MT_file_new.append(RR_topbar_file_new_draw_handler) - - -def unregister(): - - # Remove to topbar file new. - bpy.types.TOPBAR_MT_file_new.remove(RR_topbar_file_new_draw_handler) - - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/render_review/util.py b/scripts-blender/addons/blender_kitsu/render_review/util.py deleted file mode 100644 index 1da430ef..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/util.py +++ /dev/null @@ -1,44 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -import re -from typing import Union, Dict, List, Any -import bpy -from . import vars - - -def redraw_ui() -> None: - """ - Forces blender to redraw the UI. - """ - for screen in bpy.data.screens: - for area in screen.areas: - area.tag_redraw() - - -def get_version(str_value: str, format: type = str) -> Union[str, int, None]: - match = re.search(vars.VERSION_PATTERN, str_value) - if match: - version = match.group() - if format == str: - return version - if format == int: - return int(version.replace("v", "")) - return None diff --git a/scripts-blender/addons/blender_kitsu/render_review/vars.py b/scripts-blender/addons/blender_kitsu/render_review/vars.py deleted file mode 100644 index 056ce5b6..00000000 --- a/scripts-blender/addons/blender_kitsu/render_review/vars.py +++ /dev/null @@ -1,25 +0,0 @@ -# ***** BEGIN GPL LICENSE BLOCK ***** -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# ***** END GPL LICENCE BLOCK ***** -# -# (c) 2021, Blender Foundation - Paul Golter - -# These defaults will be overridden if a Kitsu project is referenced -RESOLUTION = (2048, 858) -VERSION_PATTERN = r"v\d\d\d" -FPS = 24 -DELIMITER = "-" -- 2.30.2 From 4028afcd413d283085f0345ee6526905ca1a761b Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Mon, 27 May 2024 15:25:51 -0400 Subject: [PATCH 17/17] Revert "Remove old Render Review Folder" This reverts commit 681b190462371151348887749b79fc19d35f8b67. --- .../blender_kitsu/render_review/__init__.py | 60 + .../blender_kitsu/render_review/checksqe.py | 94 ++ .../blender_kitsu/render_review/draw.py | 263 +++++ .../blender_kitsu/render_review/exception.py | 25 + .../addons/blender_kitsu/render_review/ops.py | 1043 +++++++++++++++++ .../blender_kitsu/render_review/opsdata.py | 431 +++++++ .../blender_kitsu/render_review/props.py | 102 ++ .../addons/blender_kitsu/render_review/ui.py | 178 +++ .../blender_kitsu/render_review/util.py | 44 + .../blender_kitsu/render_review/vars.py | 25 + 10 files changed, 2265 insertions(+) create mode 100644 scripts-blender/addons/blender_kitsu/render_review/__init__.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/checksqe.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/draw.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/exception.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/ops.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/opsdata.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/props.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/ui.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/util.py create mode 100644 scripts-blender/addons/blender_kitsu/render_review/vars.py diff --git a/scripts-blender/addons/blender_kitsu/render_review/__init__.py b/scripts-blender/addons/blender_kitsu/render_review/__init__.py new file mode 100644 index 00000000..29d08bdc --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/__init__.py @@ -0,0 +1,60 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +import bpy + +from . import ( + util, + props, + opsdata, + checksqe, + ops, + ui, + draw, +) + + +_need_reload = "ops" in locals() + + +if _need_reload: + import importlib + + util = importlib.reload(util) + props = importlib.reload(props) + opsdata = importlib.reload(opsdata) + checksqe = importlib.reload(checksqe) + ops = importlib.reload(ops) + ui = importlib.reload(ui) + draw = importlib.reload(draw) + + +def register(): + props.register() + ops.register() + ui.register() + draw.register() + + +def unregister(): + draw.unregister() + ui.unregister() + ops.unregister() + props.unregister() diff --git a/scripts-blender/addons/blender_kitsu/render_review/checksqe.py b/scripts-blender/addons/blender_kitsu/render_review/checksqe.py new file mode 100644 index 00000000..d6950408 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/checksqe.py @@ -0,0 +1,94 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +from typing import Dict, List, Set, Optional, Tuple, Any + +import bpy + + +def _do_ranges_collide(range1: range, range2: range) -> bool: + """Whether the two ranges collide with each other .""" + # usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140) + # first and last frame can be the same for each strip + range2 = range(range2.start + 1, range2.stop - 1) + + if not range1: + return True # empty range is subset of anything + + if not range2: + return False # non-empty range can't be subset of empty range + + if len(range1) > 1 and range1.step % range2.step: + return False # must have a single value or integer multiple step + + if range(range1.start + 1, range1.stop - 1) == range2: + return True + + if range2.start in range1 or range2[-1] in range1: + return True + + return range1.start in range2 or range1[-1] in range2 + + +def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]: + """ + Scans sequence editor and returns a dictionary. It contains a key for each channel + and a list of ranges with the occupied frame ranges as values. + """ + # {'1': [(101, 213), (300, 320)]}. + ranges: Dict[str, List[range]] = {} + + # Populate ranges. + for strip in context.scene.sequence_editor.sequences_all: + ranges.setdefault(str(strip.channel), []) + ranges[str(strip.channel)].append( + range(strip.frame_final_start, strip.frame_final_end + 1) + ) + + # Sort ranges tuple list. + for channel in ranges: + liste = ranges[channel] + ranges[channel] = sorted(liste, key=lambda item: item.start) + + return ranges + + +def get_occupied_ranges_for_strips(sequences: List[bpy.types.Sequence]) -> List[range]: + """ + Scans input list of sequences and returns a list of ranges that represent the occupied frame ranges. + """ + ranges: List[range] = [] + + # Populate ranges. + for strip in sequences: + ranges.append(range(strip.frame_final_start, strip.frame_final_end + 1)) + + # Sort ranges tuple list. + ranges.sort(key=lambda item: item.start) + return ranges + + +def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool: + for r in occupied_ranges: + # Range(101, 150). + if _do_ranges_collide(range_to_check, r): + return True + continue + return False diff --git a/scripts-blender/addons/blender_kitsu/render_review/draw.py b/scripts-blender/addons/blender_kitsu/render_review/draw.py new file mode 100644 index 00000000..20676813 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/draw.py @@ -0,0 +1,263 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# . + +# This file is copied from the blender-cloud-addon https://developer.blender.org/diffusion/BCA/ +# Author of this file is: Sybren A. Stuevel +# Modified by: Paul Golter + +import typing + +import bpy +import gpu + +APPROVED_COLOR = (0.24, 1, 0.139, 0.7) +PUSHED_TO_EDIT_COLOR = (0.8, .8, 0.1, 0.5) + +# Glsl. +gpu_vertex_shader = """ +uniform mat4 ModelViewProjectionMatrix; + +layout (location = 0) in vec2 pos; +layout (location = 1) in vec4 color; + +out vec4 lineColor; // output to the fragment shader + +void main() +{ + gl_Position = ModelViewProjectionMatrix * vec4(pos.x, pos.y, 0.0, 1.0); + lineColor = color; +} +""" + +gpu_fragment_shader = """ +out vec4 fragColor; +in vec4 lineColor; + +void main() +{ + fragColor = lineColor; +} +""" + +Float2 = typing.Tuple[float, float] +Float3 = typing.Tuple[float, float, float] +Float4 = typing.Tuple[float, float, float, float] +LINE_WIDTH = 6 + + +class LineDrawer: + def __init__(self): + self._format = gpu.types.GPUVertFormat() + self._pos_id = self._format.attr_add( + id="pos", comp_type="F32", len=2, fetch_mode="FLOAT" + ) + self._color_id = self._format.attr_add( + id="color", comp_type="F32", len=4, fetch_mode="FLOAT" + ) + + self.shader = gpu.types.GPUShader(gpu_vertex_shader, gpu_fragment_shader) + + def draw(self, coords: typing.List[Float2], colors: typing.List[Float4]): + global LINE_WIDTH + + if not coords: + return + gpu.state.blend_set("ALPHA") + gpu.state.line_width_set(LINE_WIDTH) + + vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format) + vbo.attr_fill(id=self._pos_id, data=coords) + vbo.attr_fill(id=self._color_id, data=colors) + + batch = gpu.types.GPUBatch(type="LINES", buf=vbo) + batch.program_set(self.shader) + batch.draw() + + +def get_strip_rectf(strip) -> Float4: + # Get x and y in terms of the grid's frames and channels. + x1 = strip.frame_final_start + x2 = strip.frame_final_end + # Seems to be a 5 % offset from channel top start of strip. + y1 = strip.channel + 0.05 + y2 = strip.channel - 0.05 + 1 + + return x1, y1, x2, y2 + + +def line_in_strip( + strip_coords: Float4, + pixel_size_x: float, + color: Float4, + line_height_factor: float, + out_coords: typing.List[Float2], + out_colors: typing.List[Float4], +): + # Strip coords. + s_x1, s_y1, s_x2, s_y2 = strip_coords + + # Calculate line height with factor. + line_y = (1 - line_height_factor) * s_y1 + line_height_factor * s_y2 + + # if strip is shorter than line_width use stips s_x2 + # line_x2 = s_x1 + line_width if (s_x2 - s_x1 > line_width) else s_x2 + line_x2 = s_x2 + + # Be careful not to draw over the current frame line. + cf_x = bpy.context.scene.frame_current_final + + # TODO(Sybren): figure out how to pass one colour per line, + # instead of one colour per vertex. + out_coords.append((s_x1, line_y)) + out_colors.append(color) + + if s_x1 < cf_x < line_x2: + # Bad luck, the line passes our strip, so draw two lines. + out_coords.append((cf_x - pixel_size_x, line_y)) + out_colors.append(color) + + out_coords.append((cf_x + pixel_size_x, line_y)) + out_colors.append(color) + + out_coords.append((line_x2, line_y)) + out_colors.append(color) + + +def draw_callback_px(line_drawer: LineDrawer): + global LINE_WIDTH + + context = bpy.context + + if not context.scene.sequence_editor: + return + + # From . import shown_strips. + + region = context.region + xwin1, ywin1 = region.view2d.region_to_view(0, 0) + xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height) + one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1) + pixel_size_x = one_pixel_further_x - xwin1 + + # Strips = shown_strips(context). + strips = context.scene.sequence_editor.sequences_all + + coords = [] # type: typing.List[Float2] + colors = [] # type: typing.List[Float4] + + # Collect all the lines (vertex coords + vertex colours) to draw. + for strip in strips: + + # Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords. + strip_coords = get_strip_rectf(strip) + + # Check if any of the coordinates are out of bounds. + if ( + strip_coords[0] > xwin2 + or strip_coords[2] < xwin1 + or strip_coords[1] > ywin2 + or strip_coords[3] < ywin1 + ): + continue + + if strip.rr.is_approved: + line_in_strip( + strip_coords, + pixel_size_x, + APPROVED_COLOR, + 0.05, + coords, + colors, + ) + elif strip.rr.is_pushed_to_edit: + line_in_strip( + strip_coords, + pixel_size_x, + PUSHED_TO_EDIT_COLOR, + 0.05, + coords, + colors, + ) + + line_drawer.draw(coords, colors) + + +def tag_redraw_all_sequencer_editors(): + context = bpy.context + + # Py cant access notifiers. + for window in context.window_manager.windows: + for area in window.screen.areas: + if area.type == "SEQUENCE_EDITOR": + for region in area.regions: + if region.type == "WINDOW": + region.tag_redraw() + + +# This is a list so it can be changed instead of set +# if it is only changed, it does not have to be declared as a global everywhere +cb_handle = [] + + +def callback_enable(): + global cb_handle + + if cb_handle: + return + + # Doing GPU stuff in the background crashes Blender, so let's not. + if bpy.app.background: + return + + line_drawer = LineDrawer() + cb_handle[:] = ( + bpy.types.SpaceSequenceEditor.draw_handler_add( + draw_callback_px, (line_drawer,), "WINDOW", "POST_VIEW" + ), + ) + + tag_redraw_all_sequencer_editors() + + +def callback_disable(): + global cb_handle + + if not cb_handle: + return + + try: + bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], "WINDOW") + except ValueError: + # Thrown when already removed. + pass + cb_handle.clear() + + tag_redraw_all_sequencer_editors() + + +# ---------REGISTER ----------. + + +def register(): + callback_enable() + + +def unregister(): + callback_disable() diff --git a/scripts-blender/addons/blender_kitsu/render_review/exception.py b/scripts-blender/addons/blender_kitsu/render_review/exception.py new file mode 100644 index 00000000..004c22fc --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/exception.py @@ -0,0 +1,25 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + + +class NoImageSequenceAvailableException(Exception): + """ + Error raised when trying to gather image sequence in folder but no files are existent + """ diff --git a/scripts-blender/addons/blender_kitsu/render_review/ops.py b/scripts-blender/addons/blender_kitsu/render_review/ops.py new file mode 100644 index 00000000..1d21ab44 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/ops.py @@ -0,0 +1,1043 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +import sys +import subprocess +import shutil +from pathlib import Path +from datetime import datetime +from typing import Set, Union, Optional, List, Dict, Any, Tuple +from collections import OrderedDict + +import bpy + +from . import vars, opsdata, util +from .. import prefs, cache +from ..sqe import opsdata as seq_opsdata +from ..logger import LoggerFactory + +from .exception import NoImageSequenceAvailableException + +logger = LoggerFactory.getLogger() + + +class RR_OT_sqe_create_review_session(bpy.types.Operator): + """ + Review a sequence of shots or a single shot. + Review shot will load all available preview sequences (.jpg / .png) of each found rendering + in to the sequence editor of the specified shot. + Review sequence does it for each shot in that sequence. + If user enabled use_blender_kitsu in the addon preferences, + this operator will create a linked metadata strip for the loaded shot on the top most channel. + """ + + bl_idname = "rr.sqe_create_review_session" + bl_label = "Create Review Session" + bl_description = ( + "Imports all available renderings for the specified shot / sequence " + "in to the sequence editor" + ) + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + render_dir = context.scene.rr.render_dir_path + if not render_dir: + return False + return bool( + context.scene.rr.is_render_dir_valid + and opsdata.is_shot_dir(render_dir) + or opsdata.is_sequence_dir(render_dir) + ) + + def load_strip_from_img_seq(self, context, directory, idx: int, frame_start: int = 0): + try: + # Get best preview files sequence. + image_sequence = opsdata.get_best_preview_sequence(directory) + + except NoImageSequenceAvailableException: + # if no preview files available create an empty image strip + # this assumes that when a folder is there exr sequences are available to inspect + logger.warning("%s found no preview sequence", directory.name) + exr_files = opsdata.gather_files_by_suffix( + directory, output=list, search_suffixes=[".exr"] + ) + + if not exr_files: + logger.error("%s found no exr or preview sequence", directory.name) + return + + image_sequence = exr_files[0] + logger.info("%s found %i exr frames", directory.name, len(image_sequence)) + + else: + logger.info("%s found %i preview frames", directory.name, len(image_sequence)) + + finally: + # Get frame start. + frame_start = frame_start or int(image_sequence[0].stem) + + # Create new image strip. + strip = context.scene.sequence_editor.sequences.new_image( + name=directory.name, + filepath=image_sequence[0].as_posix(), + channel=idx + 1, + frame_start=frame_start, + fit_method='ORIGINAL', + ) + + # Extend strip elements with all the available frames. + for f in image_sequence[1:]: + strip.elements.append(f.name) + + # Set strip properties. + strip.mute = True if image_sequence[0].suffix == ".exr" else False + + return strip + + def execute(self, context: bpy.types.Context) -> Set[str]: + # Clear existing strips and markers + context.scene.sequence_editor_clear() + context.scene.timeline_markers.clear() + addon_prefs = prefs.addon_prefs_get(context) + + render_dir = Path(context.scene.rr.render_dir_path) + shot_version_folders_dict = self.get_shot_folder_dict( + render_dir=render_dir, + max_versions_per_shot=addon_prefs.versions_max_count, + ) + + prev_frame_end: int = 1 + for shot_task_name, shot_version_folders in shot_version_folders_dict.items(): + shot_name = shot_task_name.split("-")[0] + logger.info("Loading versions of shot %s", shot_name) + imported_strips = self.import_shot_versions_as_strips( + context, + shot_version_folders, + frame_start=prev_frame_end, + shot_name=shot_name, + ) + + if not imported_strips: + continue + + # Query the strip that is the longest for metadata strip and prev_frame_end. + imported_strips.sort(key=lambda s: s.frame_final_duration) + strip_longest = imported_strips[-1] + prev_frame_end = strip_longest.frame_final_end + + if addon_prefs.skip_incomplete_renders: + for strip in imported_strips: + if strip.frame_final_duration < strip_longest.frame_final_duration: + context.scene.sequence_editor.sequences.remove(strip) + + # Perform kitsu operations if enabled. + if prefs.session_auth(context) and imported_strips: + if opsdata.is_active_project(): + sequence_name = shot_version_folders[0].parent.parent.parent.name + + # Create metadata strip. + metadata_strip = seq_opsdata.create_metadata_strip( + context.scene, + f"{strip_longest.name}_metadata-strip", + strip_longest.channel + 1, + strip_longest.frame_final_start, + strip_longest.frame_final_end, + ) + + logger.info( + "%s created Metadata Strip: %s", + strip_longest.name, + metadata_strip.name, + ) + + # Link metadata strip. + opsdata.link_strip_by_name(context, metadata_strip, shot_name, sequence_name) + + else: + logger.error( + "Unable to perform kitsu operations. No active project or not authorized" + ) + + # Set default scene resolution to resolution of loaded image. + render_resolution_x = vars.RESOLUTION[0] + render_resolution_y = vars.RESOLUTION[1] + project = cache.project_active_get() + # If Kitsu add-on is enabled, fetch the resolution from the online project + if project: + # TODO: make the resolution fetching a bit more robust + # Assume resolution is a string 'x' + resolution = project.resolution.split('x') + render_resolution_x = int(resolution[0]) + render_resolution_y = int(resolution[1]) + + context.scene.render.resolution_x = render_resolution_x + context.scene.render.resolution_y = render_resolution_y + + # Change frame range and frame start. + opsdata.fit_frame_range_to_strips(context) + context.scene.frame_current = context.scene.frame_start + + # Setup color management. + opsdata.setup_color_management(context) + + # scan for approved renders, will modify strip.rr.is_approved prop + # which controls the custom gpu overlay + opsdata.update_sequence_statuses(context) + + bpy.ops.sequencer.select_all(action='DESELECT') + + util.redraw_ui() + + self.report( + {"INFO"}, + f"Imported {len(imported_strips)} Render Sequences", + ) + + return {"FINISHED"} + + def get_shot_folder_dict( + self, + render_dir: str, + max_versions_per_shot=32, + ) -> OrderedDict[str, List[Path]]: + shot_version_folders: List[Path] = [] + + # If render is sequence folder user wants to review whole sequence. + if opsdata.is_sequence_dir(render_dir): + for shot_dir in render_dir.iterdir(): + # TODO: Handle case when directory is empty + shot_version_folders.extend(list(shot_dir.iterdir())) + else: + # TODO: Handle case when directory is empty + shot_version_folders.extend(list(render_dir.iterdir())) + + shot_version_folders_dict = dict() + for shot_main_folder in shot_version_folders: + shot_name = opsdata.get_shot_name_from_dir(shot_main_folder) + for shot_folder in shot_main_folder.iterdir(): + if shot_name not in shot_version_folders_dict: + shot_version_folders_dict[shot_name] = [shot_folder] + else: + shot_version_folders_dict[shot_name].append(shot_folder) + + # Sort versions by date + for shot_name, shot_folder in shot_version_folders_dict.items(): + shot_version_folders_dict[shot_name] = sorted(shot_folder, reverse=False) + # Limit list to max number of versions + shot_version_folders_dict[shot_name] = shot_version_folders_dict[shot_name][ + -max_versions_per_shot: + ] + + # Sort shots by name + sorted_dict = OrderedDict(sorted(shot_version_folders_dict.items())) + + return sorted_dict + + def import_shot_versions_as_strips( + self, + context: bpy.types.Context, + shot_version_folders: List[Path], + frame_start: int, + shot_name: str, + ) -> List[bpy.types.Sequence]: + addon_prefs = prefs.addon_prefs_get(context) + imported_strips: bpy.types.Sequence = [] + + shots_folder_to_add = [] + + if addon_prefs.shot_name_filter == "": + shots_folder_to_add = shot_version_folders + else: + for shot_folder in shot_version_folders: + if addon_prefs.shot_name_filter in shot_folder.parent.name: + shots_folder_to_add.append(shot_folder) + + for idx, shot_folder in enumerate(shots_folder_to_add): + logger.info("Processing %s", shot_folder.name) + + use_video = addon_prefs.use_video and not ( + shot_folder != shot_version_folders[-1] and addon_prefs.use_video_latest_only + ) + + shot_strip = self.import_shot_as_strip( + context, + frame_start=frame_start, + channel_idx=idx, + shot_folder=shot_folder, + shot_name=shot_name, + use_video=use_video, + ) + imported_strips.append(shot_strip) + return imported_strips + + def import_shot_as_strip( + self, + context: bpy.types.Context, + frame_start: int, + channel_idx: int, + shot_folder: Path, + shot_name: str, + use_video=False, + ) -> bpy.types.Sequence: + context.scene.timeline_markers.new(shot_name, frame=frame_start) + + # Init sequencer + if not context.scene.sequence_editor: + context.scene.sequence_editor_create() + + ### Load preview sequences in vse. + + # Compose frames found text. + frames_found_text = opsdata.gen_frames_found_text(shot_folder) + + if use_video: + video_path = opsdata.get_farm_output_mp4_path_from_folder(shot_folder) + if not video_path: + logger.warning("%s found no .mp4 preview sequence", shot_folder.name) + video_path = shot_folder + strip = context.scene.sequence_editor.sequences.new_movie( + name=shot_folder.name, + filepath=video_path.as_posix(), + channel=channel_idx + 1, + frame_start=frame_start, + fit_method='ORIGINAL', + ) + else: + strip = self.load_strip_from_img_seq(context, shot_folder, channel_idx, frame_start) + + shot_datetime = datetime.fromtimestamp(shot_folder.stat().st_mtime) + time_str = shot_datetime.strftime("%B %d, %I:%M") + strip.name = f"{shot_folder.parent.name} ({time_str})" + strip.rr.shot_name = shot_name + strip.rr.is_render = True + strip.rr.frames_found_text = frames_found_text + + return strip + + +class RR_OT_setup_review_workspace(bpy.types.Operator): + """ + Makes Video Editing Workspace active and deletes all other workspaces. + Replaces File Browser area with Image Editor. + """ + + bl_idname = "rr.setup_review_workspace" + bl_label = "Setup Review Workspace" + bl_description = ( + "Makes Video Editing Workspace active and deletes all other workspaces. " + "Replaces File Browser area with Image Editor" + ) + bl_options = {"REGISTER", "UNDO"} + + def sequences_enum_items(self, context): + return [("None", "None", "None")] + cache.get_sequences_enum_list(self, context) + + sequence: bpy.props.EnumProperty( + name="Sequence", + description="Select which sequence to review", + items=sequences_enum_items, + ) + + @staticmethod + def delayed_setup_review_workspace(): + """This function can be used as a bpy.app.timer. + It is necessary to delay certain UI changing operations that rely + on previous UI changing operations, because Blender.""" + + context = bpy.context + + for window in context.window_manager.windows: + screen = window.screen + + for area in screen.areas: + # Change video editing workspace media browser to image editor. + if area.type == "FILE_BROWSER": + area.type = "IMAGE_EDITOR" + + # Disable filepath overlay on the strips in the VSE. + if area.spaces.active.type == 'SEQUENCE_EDITOR': + area.spaces.active.timeline_overlay.show_strip_source = False + area.spaces.active.timeline_overlay.show_strip_duration = False + + if area.spaces.active.view_type == 'PREVIEW': + area.spaces.active.show_overlays = False + + def invoke(self, context, _event): + + if not cache.project_active_get(): + return self.execute(context) + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + addon_prefs = prefs.addon_prefs_get(context) + + layout.prop(self, 'sequence') + if self.sequence != 'None': + layout.row().prop(addon_prefs, 'skip_incomplete_renders') + row = layout.row() + row.prop(addon_prefs, 'use_video') + if addon_prefs.use_video: + row.prop(addon_prefs, 'use_video_latest_only') + + layout.prop(addon_prefs, 'shot_name_filter') + + def execute(self, context: bpy.types.Context) -> Set[str]: + render_dir = context.scene.rr.render_dir + + scripts_path = bpy.utils.script_paths(use_user=False)[0] + template_path = "/startup/bl_app_templates_system/Video_Editing/startup.blend" + ws_filepath = Path(scripts_path + template_path) + bpy.ops.workspace.append_activate( + idname="Video Editing", + filepath=ws_filepath.as_posix(), + ) + + # Pre-fill render directory with farm output shots directory. + addon_prefs = prefs.addon_prefs_get(bpy.context) + context.scene.rr.render_dir = addon_prefs.farm_output_dir + "/shots" + if not Path(render_dir).exists(): + self.report( + {"ERROR"}, + f"Farm Output Directory {render_dir} doesn't exist, check Add-On preferences", + ) + + return {"CANCELLED"} + + # Init sqe. + if not context.scene.sequence_editor: + context.scene.sequence_editor_create() + + # Setup color management. + opsdata.setup_color_management(bpy.context) + + self.report({"INFO"}, "Setup Render Review Workspace") + + if self.sequence and self.sequence != 'None': + cache.sequence_active_set_by_id(context, self.sequence) + context.scene.rr.render_dir += "/" + cache.sequence_active_get().name + bpy.ops.rr.sqe_create_review_session() + + # Switch File Browser to Image Editor (needs to be done with a delay). + bpy.app.timers.register(self.delayed_setup_review_workspace, first_interval=1) + + return {"FINISHED"} + + +class RR_OT_sqe_inspect_exr_sequence(bpy.types.Operator): + bl_idname = "rr.sqe_inspect_exr_sequence" + bl_label = "Inspect EXR" + bl_description = "Loads EXR sequence for selected sequence strip in image editor, if it exists" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + active_strip = context.scene.sequence_editor.active_strip + image_editor = opsdata.get_image_editor(context) + + if not active_strip: + cls.poll_message_set("No sequence strip selected") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if not image_editor: + cls.poll_message_set("No image editor open in the current workspace") + return False + + output_dir = opsdata.get_strip_folder(active_strip) + # Find exr sequence. + for f in output_dir.iterdir(): + if f.is_file() and f.suffix == ".exr": + return True + + cls.poll_message_set("Selected strip is not EXR sequence") + return False + + def execute(self, context: bpy.types.Context) -> Set[str]: + active_strip = context.scene.sequence_editor.active_strip + image_editor = opsdata.get_image_editor(context) + output_dir = opsdata.get_strip_folder(active_strip) + + # Find exr sequence. + exr_seq = [f for f in output_dir.iterdir() if f.is_file() and f.suffix == ".exr"] + + exr_seq.sort(key=lambda p: p.name) + exr_seq_frame_start = int(exr_seq[0].stem) + offset = exr_seq_frame_start - active_strip.frame_final_start + + # Remove all images with same filepath that are already loaded. + img_to_rm: bpy.types.Image = [] + for img in bpy.data.images: + if Path(bpy.path.abspath(img.filepath)) == exr_seq[0]: + img_to_rm.append(img) + + for img in img_to_rm: + bpy.data.images.remove(img) + + if bpy.app.version_string.split('.')[0] == '3': + color_space_name = "Linear" + else: + color_space_name = "Linear Rec.709" + + # Create new image datablock. + image = bpy.data.images.load(exr_seq[0].as_posix(), check_existing=True) + image.name = exr_seq[0].parent.name + "_RENDER" + image.source = "SEQUENCE" + image.colorspace_settings.name = color_space_name + + # Set active image. + image_editor.spaces.active.image = image + image_editor.spaces.active.image_user.frame_duration = 5000 + image_editor.spaces.active.image_user.frame_offset = offset + + return {"FINISHED"} + + +class RR_OT_sqe_clear_exr_inspect(bpy.types.Operator): + bl_idname = "rr.sqe_clear_exr_inspect" + bl_label = "Clear EXR Inspect" + bl_description = "Removes the active image from the image editor" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + image_editor = cls._get_image_editor(context) + if not image_editor: + cls.poll_message_set("No image editor open in the current workspace") + return False + if not image_editor.spaces.active.image: + cls.poll_message_set("No image to clear from image editor") + return False + return True + + def execute(self, context: bpy.types.Context) -> Set[str]: + image_editor = self._get_image_editor(context) + image_editor.spaces.active.image = None + return {"FINISHED"} + + @classmethod + def _get_image_editor(self, context: bpy.types.Context) -> Optional[bpy.types.Area]: + image_editor = None + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + image_editor = area + + return image_editor + + +class RR_OT_sqe_approve_render(bpy.types.Operator): + bl_idname = "rr.sqe_approve_render" + bl_label = "Push To Edit & Approve Render" + bl_description = ( + "Copies the selected strip render from the farm_output to the shot_frames directory. " + "Existing render in shot_frames will be renamed for extra backup" + ) + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + active_strip = context.scene.sequence_editor.active_strip + addon_prefs = prefs.addon_prefs_get(bpy.context) + + if not addon_prefs.shot_playblast_root_dir: + cls.poll_message_set("Playblast directory not set") + return False + + if not active_strip: + cls.poll_message_set("No sequence strip selected") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if active_strip.rr.is_approved: + cls.poll_message_set("Selected sequence strip is already approved") + return False + + return True + + def execute(self, context: bpy.types.Context) -> Set[str]: + active_strip = context.scene.sequence_editor.active_strip + + if not active_strip.rr.is_pushed_to_edit: + bpy.ops.rr.sqe_push_to_edit() + + strip_dir = opsdata.get_strip_folder(active_strip) + frames_root_dir = opsdata.get_frames_root_dir(active_strip) + shot_frames_backup_path = opsdata.get_shot_frames_backup_path(active_strip) + metadata_path = opsdata.get_shot_frames_metadata_path(active_strip) + + # Create Shot Frames path if not exists yet. + if frames_root_dir.exists(): + # Delete backup if exists. + if shot_frames_backup_path.exists(): + shutil.rmtree(shot_frames_backup_path) + + # Rename current to backup. + frames_root_dir.rename(shot_frames_backup_path) + logger.info( + "Created backup: %s > %s", + frames_root_dir.name, + shot_frames_backup_path.name, + ) + else: + frames_root_dir.mkdir(parents=True) + logger.info("Created dir in Shot Frames: %s", frames_root_dir.as_posix()) + + # Copy dir. + opsdata.copytree_verbose( + strip_dir, + frames_root_dir, + dirs_exist_ok=True, + ) + logger.info("Copied: %s \nTo: %s", strip_dir.as_posix(), frames_root_dir.as_posix()) + + # Update metadata json. + if not metadata_path.exists(): + metadata_path.touch() + opsdata.save_to_json( + {"source_current": strip_dir.as_posix(), "source_backup": ""}, + metadata_path, + ) + logger.info("Created metadata.json: %s", metadata_path.as_posix()) + else: + json_dict = opsdata.load_json(metadata_path) + # Source backup will get value from old source current. + json_dict["source_backup"] = json_dict["source_current"] + # Source current will get value from strip dir. + json_dict["source_current"] = strip_dir.as_posix() + + opsdata.save_to_json(json_dict, metadata_path) + + # Scan for approved renders. + opsdata.update_sequence_statuses(context) + util.redraw_ui() + + # Log. + self.report({"INFO"}, f"Updated {frames_root_dir.name} in Shot Frames") + logger.info("Updated metadata in: %s", metadata_path.as_posix()) + + return {"FINISHED"} + + def invoke(self, context, event): + active_strip = context.scene.sequence_editor.active_strip + frames_root_dir = opsdata.get_frames_root_dir(active_strip) + width = 200 + len(frames_root_dir.as_posix()) * 5 + return context.window_manager.invoke_props_dialog(self, width=width) + + def draw(self, context: bpy.types.Context) -> None: + layout = self.layout + active_strip = context.scene.sequence_editor.active_strip + strip_dir = opsdata.get_strip_folder(active_strip) + frames_root_dir = opsdata.get_frames_root_dir(active_strip) + + layout.separator() + layout.row(align=True).label(text="From Farm Output:", icon="RENDER_ANIMATION") + layout.row(align=True).label(text=strip_dir.as_posix()) + + layout.separator() + layout.row(align=True).label(text="To Shot Frames:", icon="FILE_TICK") + layout.row(align=True).label(text=frames_root_dir.as_posix()) + + layout.separator() + layout.row(align=True).label(text="Update Shot Frames?") + + +class RR_OT_sqe_update_sequence_statuses(bpy.types.Operator): + bl_idname = "rr.update_sequence_statuses" + bl_label = "Update Sequence Statuses" + bl_description = ( + "Scans sequence editor and updates flags for which ones are pushed " + "to the edit and which one is the currently approved version " + "by reading the metadata.json files" + ) + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + return bool(context.scene.sequence_editor.sequences_all) + + def execute(self, context: bpy.types.Context) -> Set[str]: + approved_strips = opsdata.update_sequence_statuses(context)[0] + + if approved_strips: + self.report( + {"INFO"}, + f"Found approved {'render' if len(approved_strips) == 1 else 'renders'}: {', '.join(s.name for s in approved_strips)}", + ) + else: + self.report({"INFO"}, "Found no approved renders") + return {"FINISHED"} + + +class RR_OT_open_path(bpy.types.Operator): + """ + Opens cls.filepath in explorer. Supported for win / mac / linux. + """ + + bl_idname = "rr.open_path" + bl_label = "Open Path" + bl_description = "Opens filepath in system default file browser" + + filepath: bpy.props.StringProperty( # type: ignore + name="Filepath", + description="Filepath that will be opened in explorer", + default="", + ) + + def execute(self, context: bpy.types.Context) -> Set[str]: + if not self.filepath: + self.report({"ERROR"}, "Can't open empty path in explorer") + return {"CANCELLED"} + + filepath = Path(self.filepath) + if filepath.is_file(): + filepath = filepath.parent + + if not filepath.exists(): + filepath = self._find_latest_existing_folder(filepath) + + if sys.platform == "darwin": + subprocess.check_call(["open", filepath.as_posix()]) + + elif sys.platform == "linux2" or sys.platform == "linux": + subprocess.check_call(["xdg-open", filepath.as_posix()]) + + elif sys.platform == "win32": + os.startfile(filepath.as_posix()) + + else: + self.report({"ERROR"}, f"Can't open explorer. Unsupported platform {sys.platform}") + return {"CANCELLED"} + + return {"FINISHED"} + + def _find_latest_existing_folder(self, path: Path) -> Path: + if path.exists() and path.is_dir(): + return path + else: + return self._find_latest_existing_folder(path.parent) + + +class RR_OT_sqe_isolate_strip_exit(bpy.types.Operator): + bl_idname = "rr.sqe_isolate_strip_exit" + bl_label = "Exit isolate strip view" + bl_description = "Exits isolate strip view and restores previous state" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context: bpy.types.Context) -> Set[str]: + for i in context.scene.rr.isolate_view: + try: + strip = context.scene.sequence_editor.sequences[i.name] + except KeyError: + logger.error("Exit isolate view: Strip does not exist %s", i.name) + continue + + strip.mute = i.mute + + # Clear all items. + context.scene.rr.isolate_view.clear() + + return {"FINISHED"} + + +class RR_OT_sqe_isolate_strip_enter(bpy.types.Operator): + bl_idname = "rr.sqe_isolate_strip_enter" + bl_label = "Isolate Strip" + bl_description = "Isolate all selected sequence strips, others are hidden. Previous state is saved and restorable" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + active_strip = context.scene.sequence_editor.active_strip + return bool(active_strip) + + def execute(self, context: bpy.types.Context) -> Set[str]: + sequences = list(context.scene.sequence_editor.sequences_all) + + if context.scene.rr.isolate_view.items(): + bpy.ops.rr.sqe_isolate_strip_exit() + + # Mute all and save state to restore later. + for s in sequences: + # Save this state to restore it later. + item = context.scene.rr.isolate_view.add() + item.name = s.name + item.mute = s.mute + s.mute = True + + # Unmute selected. + for s in context.selected_sequences: + s.mute = False + + return {"FINISHED"} + + +class RR_OT_sqe_push_to_edit(bpy.types.Operator): + """ + This operator pushes the active render strip to the edit. Only .mp4 files will be pushed to edit. + If the .mp4 file is not existent but the preview .jpg sequence is in the render folder. This operator + creates an .mp4 with ffmpeg. The .mp4 file will be named after the flamenco naming convention, but when + copied over to the Shot Previews it will be renamed and gets a version string. + """ + + bl_idname = "rr.sqe_push_to_edit" + bl_label = "Push To Edit" + bl_description = ( + "Copies .mp4 file of current sequence strip to the shot preview directory with " + "auto version incrementation. " + "Creates .mp4 with ffmpeg if not existent yet" + ) + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + addon_prefs = prefs.addon_prefs_get(context) + active_strip = context.scene.sequence_editor.active_strip + + if not addon_prefs.shot_playblast_root_dir: + cls.poll_message_set("No shot playblast root dir set") + return False + + if not active_strip: + cls.poll_message_set("No active strip") + return False + + if not active_strip.rr.is_render: + cls.poll_message_set("Selected sequence strip is not an imported render") + return False + + if not active_strip.rr.is_pushed_to_edit: + cls.poll_message_set("Selected sequence strip is already pushed to edit") + return False + return True + + def execute(self, context: bpy.types.Context) -> Set[str]: + active_strip = context.scene.sequence_editor.active_strip + + render_dir = opsdata.get_strip_folder(active_strip) + shot_previews_dir = opsdata.get_shot_previews_path(active_strip) + metadata_path = shot_previews_dir / "metadata.json" + + # -------------GET MP4 OR CREATE WITH FFMPEG --------------- + # Trying to get render_mp4_path will throw error if no jpg files are available. + try: + mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) + except NoImageSequenceAvailableException: + # No jpeg files available. + self.report({"ERROR"}, f"No preview files available in {render_dir.as_posix()}") + return {"CANCELLED"} + + # If mp4 path does not exist, use ffmpeg to create preview file. + if not mp4_path.exists(): + preview_files = opsdata.get_best_preview_sequence(render_dir) + fffmpeg_command = f"ffmpeg -start_number {int(preview_files[0].stem)} -framerate {vars.FPS} -i {render_dir.as_posix()}/%06d{preview_files[0].suffix} -c:v libx264 -preset medium -crf 23 -pix_fmt yuv420p {mp4_path.as_posix()}" + logger.info("Creating .mp4 with ffmpeg") + subprocess.call(fffmpeg_command, shell=True) + logger.info("Created .mp4: %s", mp4_path.as_posix()) + else: + logger.info("Found existing .mp4 file: %s", mp4_path.as_posix()) + + # --------------COPY MP4 TO Shot Previews ----------------. + + # Create edit path if not exists yet. + if not shot_previews_dir.exists(): + shot_previews_dir.mkdir(parents=True) + logger.info("Created dir in Shot Previews: %s", shot_previews_dir.as_posix()) + + # Get edit_filepath. + edit_filepath = self.get_edit_filepath(active_strip) + + # Copy mp4 to edit filepath. + shutil.copy2(mp4_path.as_posix(), edit_filepath.as_posix()) + logger.info("Copied: %s \nTo: %s", mp4_path.as_posix(), edit_filepath.as_posix()) + + # ----------------UPDATE METADATA.JSON ------------------. + + # Create metadata json. + if not metadata_path.exists(): + metadata_path.touch() + logger.info("Created metadata.json: %s", metadata_path.as_posix()) + opsdata.save_to_json({}, metadata_path) + + # Udpate metadata json. + json_obj = opsdata.load_json(metadata_path) + json_obj[edit_filepath.name] = mp4_path.as_posix() + opsdata.save_to_json( + json_obj, + metadata_path, + ) + logger.info("Updated metadata in: %s", metadata_path.as_posix()) + + # Scan for approved renders. + opsdata.update_sequence_statuses(context) + + # Log. + self.report( + {"INFO"}, + f"Pushed to edit: {edit_filepath.as_posix()}", + ) + return {"FINISHED"} + + def draw(self, context: bpy.types.Context) -> None: + layout = self.layout + active_strip = context.scene.sequence_editor.active_strip + edit_filepath = self.get_edit_filepath(active_strip) + + try: + mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) + except NoImageSequenceAvailableException: + layout.separator() + layout.row(align=True).label(text="No preview files available", icon="ERROR") + return + + text = "From Farm Output:" + if not mp4_path.exists(): + text = "From Farm Output (will be created with ffmpeg):" + + layout.separator() + layout.row(align=True).label(text=text, icon="RENDER_ANIMATION") + layout.row(align=True).label(text=mp4_path.as_posix()) + + layout.separator() + layout.row(align=True).label(text="To Shot Previews:", icon="FILE_TICK") + layout.row(align=True).label(text=edit_filepath.as_posix()) + + layout.separator() + layout.row(align=True).label(text="Copy to Shot Previews?") + + def invoke(self, context, event): + active_strip = context.scene.sequence_editor.active_strip + try: + mp4_path = Path(opsdata.get_farm_output_mp4_path(active_strip)) + except NoImageSequenceAvailableException: + width = 200 + else: + width = 200 + len(mp4_path.as_posix()) * 5 + return context.window_manager.invoke_props_dialog(self, width=width) + + def get_edit_filepath(self, strip: bpy.types.Sequence) -> Path: + delimiter = vars.DELIMITER + render_dir = opsdata.get_strip_folder(strip) + shot_previews_dir = opsdata.get_shot_previews_path(strip) + + # Find latest edit version. + existing_files: List[Path] = [] + increment = "v001" + if shot_previews_dir.exists(): + for file in shot_previews_dir.iterdir(): + if not file.is_file(): + continue + + if not file.name.startswith(opsdata.get_shot_dot_task_type(render_dir)): + continue + + version = util.get_version(file.name) + if not version: + continue + + if ( + file.name.replace(version, "") + == f"{opsdata.get_shot_dot_task_type(render_dir)}{delimiter}.mp4" + ): + existing_files.append(file) + + existing_files.sort(key=lambda f: f.name) + + # Get version string. + if len(existing_files) > 0: + latest_version = util.get_version(existing_files[-1].name) + increment = "v{:03}".format(int(latest_version.replace("v", "")) + 1) + + # Compose edit filepath of new mp4 file. + edit_filepath = ( + shot_previews_dir + / f"{opsdata.get_shot_dot_task_type(render_dir)}{delimiter}{increment}.mp4" + ) + return edit_filepath + + +# ----------------REGISTER--------------. + + +classes = [ + RR_OT_sqe_create_review_session, + RR_OT_setup_review_workspace, + RR_OT_sqe_inspect_exr_sequence, + RR_OT_sqe_clear_exr_inspect, + RR_OT_sqe_approve_render, + RR_OT_sqe_update_sequence_statuses, + RR_OT_open_path, + RR_OT_sqe_isolate_strip_enter, + RR_OT_sqe_isolate_strip_exit, + RR_OT_sqe_push_to_edit, +] + +addon_keymap_items = [] + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + # register hotkeys + # does not work if blender runs in background + if not bpy.app.background: + global addon_keymap_items + keymap = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name="Window") + + # Isolate strip. + addon_keymap_items.append( + keymap.keymap_items.new("rr.sqe_isolate_strip_enter", value="PRESS", type="ONE") + ) + + # Umute all. + addon_keymap_items.append( + keymap.keymap_items.new( + "rr.sqe_isolate_strip_exit", value="PRESS", type="ONE", alt=True + ) + ) + for kmi in addon_keymap_items: + logger.info("Registered new hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + # Does not work if blender runs in background. + if not bpy.app.background: + global addon_keymap_items + # Remove hotkeys. + keymap = bpy.context.window_manager.keyconfigs.addon.keymaps["Window"] + for kmi in addon_keymap_items: + logger.info("Remove hotkey: %s : %s", kmi.type, kmi.properties.bl_rna.name) + keymap.keymap_items.remove(kmi) + + addon_keymap_items.clear() diff --git a/scripts-blender/addons/blender_kitsu/render_review/opsdata.py b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py new file mode 100644 index 00000000..231b77d8 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/opsdata.py @@ -0,0 +1,431 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +import json +import shutil +from pathlib import Path +from typing import Set, Union, Optional, List, Dict, Any, Tuple + +import bpy + +from . import vars, checksqe, util +from .. import prefs, cache +from ..sqe import opsdata as sqe_opsdata +from .exception import NoImageSequenceAvailableException + +from ..logger import LoggerFactory + +logger = LoggerFactory.getLogger() + + +copytree_list: List[Path] = [] +copytree_num_of_items: int = 0 + + +def copytree_verbose(src: Union[str, Path], dest: Union[str, Path], **kwargs): + _copytree_init_progress_update(Path(src)) + shutil.copytree(src, dest, copy_function=_copy2_tree_progress, **kwargs) + _copytree_clear_progress_update() + + +def _copytree_init_progress_update(source_dir: Path): + global copytree_num_of_items + file_list = [f for f in source_dir.glob("**/*") if f.is_file()] + copytree_num_of_items = len(file_list) + + +def _copy2_tree_progress(src, dst): + """ + Function that can be used for copy_function + argument on shutil.copytree function. + Logs every item that is currently copied. + """ + global copytree_num_of_items + global copytree_list + + copytree_list.append(Path(src)) + progress = round((len(copytree_list) * 100) / copytree_num_of_items) + logger.info("Copying %s (%i%%)", src, progress) + shutil.copy2(src, dst) + + +def _copytree_clear_progress_update(): + global copytree_num_of_items + + copytree_num_of_items = 0 + copytree_list.clear() + + +def get_valid_cs_sequences( + context: bpy.types.Context, sequence_list: List[bpy.types.Sequence] = [] +) -> List[bpy.types.Sequence]: + + sequences: List[bpy.types.Sequence] = [] + + if sequence_list: + sequences = sequence_list + else: + sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all + + if cache.project_active_get(): + + valid_sequences = [ + s + for s in sequences + if s.type in ["MOVIE", "IMAGE"] and not s.mute and not s.kitsu.initialized + ] + else: + valid_sequences = [s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute] + + return valid_sequences + + +def get_frames_root_dir(strip: bpy.types.Sequence) -> Path: + # sf = shot_frames | fo = farm_output. + addon_prefs = prefs.addon_prefs_get(bpy.context) + fo_dir = get_strip_folder(strip) + sf_dir = addon_prefs.frames_root_dir / fo_dir.parent.relative_to(fo_dir.parents[3]) + + return sf_dir + + +def get_strip_folder(strip: bpy.types.Sequence) -> Path: + if hasattr(strip, 'directory'): + return Path(strip.directory) + else: + return Path(strip.filepath).parent + + +def get_shot_previews_path(strip: bpy.types.Sequence) -> Path: + # Fo > farm_output. + addon_prefs = prefs.addon_prefs_get(bpy.context) + fo_dir = get_strip_folder(strip) + shot_previews_dir = addon_prefs.shot_playblast_root_dir / fo_dir.parent.relative_to( + fo_dir.parents[3] + ) + + return shot_previews_dir + + +def get_shot_dot_task_type(path: Path): + return path.parent.name + + +def get_farm_output_mp4_path(strip: bpy.types.Sequence) -> Path: + render_dir = get_strip_folder(strip) + return get_farm_output_mp4_path_from_folder(render_dir) + + +def get_farm_output_mp4_path_from_folder(render_dir: str) -> Path: + render_dir = Path(render_dir) + shot_name = render_dir.parent.name + + # 070_0040_A.lighting-101-136.mp4 #farm always does .lighting not .comp + # because flamenco writes in and out frame in filename we need check the first and + # last frame in the folder + preview_seq = get_best_preview_sequence(render_dir) + + mp4_filename = f"{shot_name}-{int(preview_seq[0].stem)}-{int(preview_seq[-1].stem)}.mp4" + + return render_dir / mp4_filename + + +def get_best_preview_sequence(dir: Path) -> List[Path]: + + files: List[List[Path]] = gather_files_by_suffix( + dir, output=dict, search_suffixes=[".jpg", ".png"] + ) + if not files: + raise NoImageSequenceAvailableException(f"No preview files found in: {dir.as_posix()}") + + # Select the right images sequence. + if len(files) == 1: + # If only one image sequence available take that. + preview_seq = files[list(files.keys())[0]] + + # Both jpg and png available. + else: + # If same amount of frames take png. + if len(files[".jpg"]) == len(files[".png"]): + preview_seq = files[".png"] + else: + # If not, take whichever is longest. + preview_seq = [files[".jpg"], files[".png"]].sort(key=lambda x: len(x))[-1] + + return preview_seq + + +def get_shot_frames_backup_path(strip: bpy.types.Sequence) -> Path: + fs_dir = get_frames_root_dir(strip) + return fs_dir.parent / f"_backup.{fs_dir.name}" + + +def get_shot_frames_metadata_path(strip: bpy.types.Sequence) -> Path: + fs_dir = get_frames_root_dir(strip) + return fs_dir.parent / "metadata.json" + + +def get_shot_previews_metadata_path(strip: bpy.types.Sequence) -> Path: + fs_dir = get_shot_previews_path(strip) + return fs_dir / "metadata.json" + + +def load_json(path: Path) -> Any: + with open(path.as_posix(), "r") as file: + obj = json.load(file) + return obj + + +def save_to_json(obj: Any, path: Path) -> None: + with open(path.as_posix(), "w") as file: + json.dump(obj, file, indent=4) + + +def update_sequence_statuses( + context: bpy.types.Context, +) -> List[bpy.types.Sequence]: + return update_is_approved(context), update_is_pushed_to_edit(context) + + +def update_is_approved( + context: bpy.types.Context, +) -> List[bpy.types.Sequence]: + sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] + + approved_strips = [] + + for s in sequences: + metadata_path = get_shot_frames_metadata_path(s) + if not metadata_path.exists(): + continue + json_obj = load_json(metadata_path) # TODO: prevent opening same json multi times + + if Path(json_obj["source_current"]) == get_strip_folder(s): + s.rr.is_approved = True + approved_strips.append(s) + logger.info("Detected approved strip: %s", s.name) + else: + s.rr.is_approved = False + + return approved_strips + + +def update_is_pushed_to_edit( + context: bpy.types.Context, +) -> List[bpy.types.Sequence]: + sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render] + + pushed_strips = [] + + for s in sequences: + metadata_path = get_shot_previews_metadata_path(s) + if not metadata_path.exists(): + continue + + json_obj = load_json(metadata_path) + + valid_paths = {Path(value).parent for _key, value in json_obj.items()} + + if get_strip_folder(s) in valid_paths: + s.rr.is_pushed_to_edit = True + pushed_strips.append(s) + logger.info("Detected pushed strip: %s", s.name) + else: + s.rr.is_pushed_to_edit = False + + return pushed_strips + + +def gather_files_by_suffix( + dir: Path, output=str, search_suffixes: List[str] = [".jpg", ".png", ".exr"] +) -> Union[str, List, Dict]: + """ + Gathers files in dir that end with an extension in search_suffixes. + Supported values for output: str, list, dict + """ + + files: Dict[str, List[Path]] = {} + + # Gather files. + for f in dir.iterdir(): + if not f.is_file(): + continue + + for suffix in search_suffixes: + if f.suffix == suffix: + files.setdefault(suffix, []) + files[suffix].append(f) + + # Sort. + for suffix, file_list in files.items(): + files[suffix] = sorted(file_list, key=lambda f: f.name) + + # Return. + if output == str: + return_str = "" + for suffix, file_list in files.items(): + return_str += f" | {suffix}: {len(file_list)}" + + # Replace first occurence, we dont want that at the beginning. + return_str = return_str.replace(" | ", "", 1) + + return return_str + + elif output == dict: + return files + + elif output == list: + output_list = [] + for suffix, file_list in files.items(): + output_list.append(file_list) + + return output_list + else: + raise ValueError( + f"Supported output types are: str, dict, list. {str(output)} not implemented yet." + ) + + +def gen_frames_found_text(dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"]) -> str: + files_dict = gather_files_by_suffix(dir, output=dict, search_suffixes=search_suffixes) + + frames_found_text = "" # frames found text will be used in ui + for suffix, file_list in files_dict.items(): + frames_found_text += f" | {suffix}: {len(file_list)}" + + # Replace first occurence, we dont want that at the beginning. + frames_found_text = frames_found_text.replace( + " | ", + "", + 1, + ) + return frames_found_text + + +def is_sequence_dir(dir: Path) -> bool: + return dir.parent.name == "shots" + + +def is_shot_dir(dir: Path) -> bool: + return dir.parent.parent.name == "shots" + + +def get_shot_name_from_dir(dir: Path) -> str: + return dir.stem # 060_0010_A.lighting > 060_0010_A + + +def get_image_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]: + image_editor = None + + for area in context.screen.areas: + if area.type == "IMAGE_EDITOR": + image_editor = area + + return image_editor + + +def get_sqe_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]: + sqe_editor = None + + for area in context.screen.areas: + if area.type == "SEQUENCE_EDITOR": + sqe_editor = area + + return sqe_editor + + +def fit_frame_range_to_strips( + context: bpy.types.Context, strips: Optional[List[bpy.types.Sequence]] = None +) -> Tuple[int, int]: + def get_sort_tuple(strip: bpy.types.Sequence) -> Tuple[int, int]: + return (strip.frame_final_start, strip.frame_final_duration) + + if not strips: + strips = context.scene.sequence_editor.sequences_all + + if not strips: + return (0, 0) + + strips = list(strips) + strips.sort(key=get_sort_tuple) + + context.scene.frame_start = strips[0].frame_final_start + context.scene.frame_end = strips[-1].frame_final_end - 1 + + return (context.scene.frame_start, context.scene.frame_end) + + +def get_top_level_valid_strips_continious( + context: bpy.types.Context, +) -> List[bpy.types.Sequence]: + + sequences_tmp = get_valid_cs_sequences( + context, sequence_list=list(context.scene.sequence_editor.sequences_all) + ) + + sequences_tmp.sort(key=lambda s: (s.channel, s.frame_final_start), reverse=True) + sequences: List[bpy.types.Sequence] = [] + + for strip in sequences_tmp: + + occ_ranges = checksqe.get_occupied_ranges_for_strips(sequences) + s_range = range(strip.frame_final_start, strip.frame_final_end + 1) + + if not checksqe.is_range_occupied(s_range, occ_ranges): + sequences.append(strip) + + return sequences + + +def setup_color_management(context: bpy.types.Context) -> None: + if context.scene.view_settings.view_transform != 'Standard': + context.scene.view_settings.view_transform = 'Standard' + logger.info("Set view transform to: Standard") + + +def is_active_project() -> bool: + return bool(cache.project_active_get()) + + +def link_strip_by_name( + context: bpy.types.Context, + strip: bpy.types.Sequence, + shot_name: str, + sequence_name: str, +) -> None: + # Get seq and shot. + active_project = cache.project_active_get() + seq = active_project.get_sequence_by_name(sequence_name) + shot = active_project.get_shot_by_name(seq, shot_name) + + if not shot: + logger.error("Unable to find shot %s on kitsu", shot_name) + return + + sqe_opsdata.link_metadata_strip(context, shot, seq, strip) + + # Log. + t = "Linked strip: %s to shot: %s with ID: %s" % ( + strip.name, + shot.name, + shot.id, + ) + logger.info(t) + util.redraw_ui() diff --git a/scripts-blender/addons/blender_kitsu/render_review/props.py b/scripts-blender/addons/blender_kitsu/render_review/props.py new file mode 100644 index 00000000..a6be5b28 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/props.py @@ -0,0 +1,102 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +from typing import Set, Union, Optional, List, Dict, Any +from pathlib import Path + +import bpy + + +from ..logger import LoggerFactory + +logger = LoggerFactory.getLogger() + + +class RR_isolate_collection_prop(bpy.types.PropertyGroup): + mute: bpy.props.BoolProperty() + + +class RR_property_group_scene(bpy.types.PropertyGroup): + """""" + + render_dir: bpy.props.StringProperty(name="Render Directory", subtype="DIR_PATH") + isolate_view: bpy.props.CollectionProperty(type=RR_isolate_collection_prop) + + @property + def render_dir_path(self): + if not self.is_render_dir_valid: + return None + return Path(bpy.path.abspath(self.render_dir)).absolute() + + @property + def is_render_dir_valid(self) -> bool: + if not self.render_dir: + return False + + if not bpy.data.filepath and self.render_dir.startswith("//"): + return False + + return True + + +class RR_property_group_sequence(bpy.types.PropertyGroup): + """ + Property group that will be registered on sequence strips. + """ + + is_render: bpy.props.BoolProperty(name="Is Render") + is_approved: bpy.props.BoolProperty(name="Is Approved") + is_pushed_to_edit: bpy.props.BoolProperty(name="Is Pushed To Edit") + frames_found_text: bpy.props.StringProperty(name="Frames Found") + shot_name: bpy.props.StringProperty(name="Shot") + + +# ----------------REGISTER--------------. + +classes = [ + RR_isolate_collection_prop, + RR_property_group_scene, + RR_property_group_sequence, +] + + +def register(): + + for cls in classes: + bpy.utils.register_class(cls) + + # Scene Properties. + bpy.types.Scene.rr = bpy.props.PointerProperty( + name="Render Review", + type=RR_property_group_scene, + description="Metadata that is required for render_review", + ) + + # Sequence Properties. + bpy.types.Sequence.rr = bpy.props.PointerProperty( + name="Render Review", + type=RR_property_group_sequence, + description="Metadata that is required for render_review", + ) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/render_review/ui.py b/scripts-blender/addons/blender_kitsu/render_review/ui.py new file mode 100644 index 00000000..91c73e15 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/ui.py @@ -0,0 +1,178 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +from pathlib import Path + +from typing import Set, Union, Optional, List, Dict, Any + +import bpy + +from .ops import ( + RR_OT_sqe_create_review_session, + RR_OT_setup_review_workspace, + RR_OT_sqe_inspect_exr_sequence, + RR_OT_sqe_clear_exr_inspect, + RR_OT_sqe_approve_render, + RR_OT_sqe_update_sequence_statuses, + RR_OT_open_path, + RR_OT_sqe_push_to_edit, +) +from . import opsdata +from .. import prefs + + +class RR_PT_render_review(bpy.types.Panel): + """ """ + + bl_category = "Render Review" + bl_label = "Render Review" + bl_space_type = "SEQUENCE_EDITOR" + bl_region_type = "UI" + bl_order = 10 + + def draw(self, context: bpy.types.Context) -> None: + + addon_prefs = prefs.addon_prefs_get(context) + + # Create box. + layout = self.layout + box = layout.box() + + # Label and setup workspace. + row = box.row(align=True) + row.label(text="Review", icon="CAMERA_DATA") + row.operator(RR_OT_setup_review_workspace.bl_idname, text="", icon="WINDOW") + + # Render dir prop. + row = box.row(align=True) + row.prop(context.scene.rr, "render_dir") + + # Create session. + render_dir = context.scene.rr.render_dir_path + text = f"Invalid Render Directory" + if render_dir: + if opsdata.is_sequence_dir(render_dir): + text = f"Review Sequence: {render_dir.name}" + elif opsdata.is_shot_dir(render_dir): + text = f"Review Shot: {render_dir.stem}" + + row = box.row(align=True) + row.operator(RR_OT_sqe_create_review_session.bl_idname, text=text, icon="PLAY") + row = box.row(align=True) + row.prop(addon_prefs, 'use_video') + if addon_prefs.use_video: + row.prop(addon_prefs, 'use_video_latest_only') + + # Warning if kitsu on but not logged in. + if not prefs.session_auth(context): + row = box.split(align=True, factor=0.7) + row.label(text="Kitsu enabled but not logged in", icon="ERROR") + row.operator("kitsu.session_start", text="Login") + + elif not opsdata.is_active_project(): + row = box.row(align=True) + row.label(text="Kitsu enabled but no active project", icon="ERROR") + + sqe = context.scene.sequence_editor + if not sqe: + return + active_strip = sqe.active_strip + if active_strip and active_strip.rr.is_render: + # Create box. + layout = self.layout + box = layout.box() + box.label(text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF") + box.separator() + + # Render dir name label and open file op. + row = box.row(align=True) + directory = opsdata.get_strip_folder(active_strip) + row.label(text=f"Folder: {directory.name}") + row.operator( + RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="", emboss=False + ).filepath = bpy.path.abspath(directory.as_posix()) + + # Nr of frames. + box.row(align=True).label(text=f"Frames: {active_strip.rr.frames_found_text}") + + # Inspect exr. + text = "Inspect EXR" + icon = "VIEWZOOM" + if not opsdata.get_image_editor(context): + text = "Inspect EXR: Needs Image Editor" + icon = "ERROR" + + row = box.row(align=True) + row.operator(RR_OT_sqe_inspect_exr_sequence.bl_idname, icon=icon, text=text) + row.operator(RR_OT_sqe_clear_exr_inspect.bl_idname, text="", icon="X") + + # Approve render & udpate approved. + row = box.row(align=True) + + text = "Push To Edit & Approve Render" + if active_strip.rr.is_pushed_to_edit: + text = "Approve Render" + row.operator(RR_OT_sqe_approve_render.bl_idname, icon="CHECKMARK", text=text) + row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH") + + # Push to edit. + if not addon_prefs.shot_playblast_root_dir: + shot_previews_dir = "" # ops handle invalid path + else: + shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix() + + row = box.row(align=True) + row.operator(RR_OT_sqe_push_to_edit.bl_idname, icon="EXPORT") + row.operator(RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="").filepath = ( + shot_previews_dir + ) + + # Push strip to Kitsu. + box.row().operator('kitsu.sqe_push_shot', icon='URL') + + +def RR_topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None: + layout = self.layout + op = layout.operator(RR_OT_setup_review_workspace.bl_idname, text="Render Review") + + +# ----------------REGISTER--------------. + +classes = [ + RR_PT_render_review, +] + + +def register(): + + for cls in classes: + bpy.utils.register_class(cls) + + # Append to topbar file new. + bpy.types.TOPBAR_MT_file_new.append(RR_topbar_file_new_draw_handler) + + +def unregister(): + + # Remove to topbar file new. + bpy.types.TOPBAR_MT_file_new.remove(RR_topbar_file_new_draw_handler) + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/render_review/util.py b/scripts-blender/addons/blender_kitsu/render_review/util.py new file mode 100644 index 00000000..1da430ef --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/util.py @@ -0,0 +1,44 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +import re +from typing import Union, Dict, List, Any +import bpy +from . import vars + + +def redraw_ui() -> None: + """ + Forces blender to redraw the UI. + """ + for screen in bpy.data.screens: + for area in screen.areas: + area.tag_redraw() + + +def get_version(str_value: str, format: type = str) -> Union[str, int, None]: + match = re.search(vars.VERSION_PATTERN, str_value) + if match: + version = match.group() + if format == str: + return version + if format == int: + return int(version.replace("v", "")) + return None diff --git a/scripts-blender/addons/blender_kitsu/render_review/vars.py b/scripts-blender/addons/blender_kitsu/render_review/vars.py new file mode 100644 index 00000000..056ce5b6 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/render_review/vars.py @@ -0,0 +1,25 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2021, Blender Foundation - Paul Golter + +# These defaults will be overridden if a Kitsu project is referenced +RESOLUTION = (2048, 858) +VERSION_PATTERN = r"v\d\d\d" +FPS = 24 +DELIMITER = "-" -- 2.30.2