# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later import math from pathlib import Path from typing import Set, List import bpy from . import prefs, opsdata from .log import LoggerFactory from .geo_seq import SequenceRect from .geo import Grid, NestedRectangle logger = LoggerFactory.getLogger(name=__name__) class CS_OT_make_contactsheet(bpy.types.Operator): """ This operator creates a contactsheet out of the selected sequence strips. The contactsheet will be created in a separate scene. """ bl_idname = "contactsheet.make_contactsheet" bl_label = "Make Contact Sheet" bl_description = ( "Creates a temporary scene and arranges the previously selected sequences in a grid. " "If no sequences were selected it takes a continuous row of the top most sequences" ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: return opsdata.poll_make_contactsheet(context) def execute(self, context: bpy.types.Context) -> Set[str]: addon_prefs = prefs.addon_prefs_get(context) # Gather sequences to process. sequences = context.selected_sequences if not sequences: # If nothing selected take a continuous row of the top most sequences. sequences = opsdata.get_top_level_valid_strips_continuous(context) else: sequences = opsdata.get_valid_cs_sequences(sequences) # Select sequences, will remove sequences later that are not selected. bpy.ops.sequencer.select_all(action="DESELECT") for s in sequences: s.select = True row_count = None start_frame = 1 # Get contactsheet metadata. sqe_editor = opsdata.get_sqe_editor(context) orig_proxy_render_size = sqe_editor.spaces.active.proxy_render_size orig_use_proxies = sqe_editor.spaces.active.use_proxies # Create new scene. scene_orig = bpy.context.scene bpy.ops.scene.new(type="FULL_COPY") # Changes active scene, makes copy. scene_tmp = bpy.context.scene scene_tmp.name = "contactsheet" logger.info("Created temporary scene for contactsheet: %s", scene_tmp.name) # Save contactsheet metadata. sqe_editor = opsdata.get_sqe_editor(context) sqe_editor.spaces.active.proxy_render_size = "PROXY_25" sqe_editor.spaces.active.use_proxies = True scene_tmp.contactsheet.is_contactsheet = True scene_tmp.contactsheet.contactsheet_meta.scene = scene_orig scene_tmp.contactsheet.contactsheet_meta.use_proxies = orig_use_proxies scene_tmp.contactsheet.contactsheet_meta.proxy_render_size = ( orig_proxy_render_size ) # Remove sequences in new scene that are not selected. seq_rm: List["bpy.types.Strip"] = [ s for s in scene_tmp.sequence_editor.sequences_all if not s.select ] for s in seq_rm: scene_tmp.sequence_editor.sequences.remove(s) # Get all sequences in new scene and sort them. sequences = list(scene_tmp.sequence_editor.sequences_all) sequences.sort(key=lambda strip: (strip.frame_final_start, strip.channel)) # Place black color strip in channel 1. color_strip = context.scene.sequence_editor.sequences.new_effect( "background", "COLOR", 1, start_frame, frame_end=start_frame + 1 ) color_strip.color = (0, 0, 0) # Create required number of Metadata Strips to workaround the limit of 32 channels. nr_of_metadata_strips = math.ceil(len(sequences) / 32) metadata_strips: List["bpy.types.Strip"] = [] for i in range(nr_of_metadata_strips): channel = i + 2 metadata_strip = context.scene.sequence_editor.sequences.new_meta( f"contactsheet_meta_{channel-1}", channel, start_frame ) metadata_strips.append(metadata_strip) logger.debug("Created Metadata Strip: %s", metadata_strip.name) # Move sequences in to Metadata Strips, place them on top of each other # make them start at the same frame. for idx, seq in enumerate(sequences): # Move to Metadata Strip. channel = idx + 1 meta_index = math.floor(idx / 32) seq.move_to_meta(metadata_strips[meta_index]) # Set seq properties inside Metadata Strip. seq.channel = channel - ((meta_index) * 32) seq.frame_start = start_frame seq.blend_type = "ALPHA_OVER" # Elongate all strips to the strip with the longest duration. tmp_sequences = sorted(sequences, key=lambda s: s.frame_final_end) tmp_sequences.insert(0, color_strip) max_end: int = tmp_sequences[-1].frame_final_end for strip in tmp_sequences: if strip.frame_final_end < max_end: strip.frame_final_end = max_end # Clip the Metadata Strip frame end at max end and set alpha over. for strip in metadata_strips: strip.frame_start = start_frame strip.frame_final_end = max_end strip.blend_type = "ALPHA_OVER" # Scene settings. # Change frame range and frame start. self.set_render_settings(context) self.set_output_path(context) self.set_sqe_area_settings(context) # Create content list for grid. sqe_rects: List[SequenceRect] = [SequenceRect(seq) for seq in sequences] content: List[NestedRectangle] = [ NestedRectangle(0, 0, srect.width, srect.height, child=srect) for srect in sqe_rects ] # Create grid. if context.scene.contactsheet.use_custom_rows: row_count = context.scene.contactsheet.rows grid = Grid.from_content( 0, 0, context.scene.contactsheet.contactsheet_x, context.scene.contactsheet.contactsheet_y, content, row_count=row_count, ) grid.scale_content(addon_prefs.contactsheet_scale_factor) return {"FINISHED"} def set_sqe_area_settings(self, context: bpy.types.Context) -> None: sqe_editor = opsdata.get_sqe_editor(context) sqe_editor.spaces.active.proxy_render_size = "PROXY_25" sqe_editor.spaces.active.use_proxies = True def set_render_settings(self, context: bpy.types.Context) -> None: opsdata.fit_frame_range_to_strips(context) context.scene.frame_current = context.scene.frame_start context.scene.render.resolution_x = context.scene.contactsheet.contactsheet_x context.scene.render.resolution_y = context.scene.contactsheet.contactsheet_y context.scene.render.image_settings.file_format = "PNG" context.scene.render.image_settings.color_mode = "RGB" context.scene.render.image_settings.color_depth = "8" context.scene.render.image_settings.compression = 15 def set_output_path(self, context: bpy.types.Context) -> None: addon_prefs = prefs.addon_prefs_get(context) cs_dir: Path = addon_prefs.contactsheet_dir_path output_path: str = "" # File not saved and cs_dir not available. if not bpy.data.filepath and not cs_dir: logger.warning( "Failed to set output settings. Contactsheet Output Directory " "not defined in addon preferences and file not saved." ) return # File saved and cs_dir available. if cs_dir and bpy.data.filepath: output_path = cs_dir.joinpath( f"{Path(bpy.data.filepath).stem}_contactsheet.png" ).as_posix() # File not saved but cs_dir available. elif cs_dir: output_path = cs_dir.joinpath(f"contactsheet.png").as_posix() # File saved but cs_dir not available. else: output_path = ( Path(bpy.data.filepath) .parent.joinpath(f"{Path(bpy.data.filepath).stem}_contactsheet.png") .as_posix() ) # Set output path. context.scene.render.filepath = output_path class CS_OT_exit_contactsheet(bpy.types.Operator): bl_idname = "contactsheet.exit_contactsheet" bl_label = "Exit Contact Sheet" bl_description = ( "Exits contactsheet scene, deletes it and " "return to original scene that was used to create the contactsheet" ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: return bool(context.scene.contactsheet.is_contactsheet) def execute(self, context: bpy.types.Context) -> Set[str]: cs_scene = context.scene cs_scene_name = cs_scene.name # Change active scene to orig scene. context.window.scene = context.scene.contactsheet.contactsheet_meta.scene # Restore proxy settings from contactsheet.contactsheet_meta. sqe_editor = opsdata.get_sqe_editor(context) sqe_editor.spaces.active.proxy_render_size = ( cs_scene.contactsheet.contactsheet_meta.proxy_render_size ) sqe_editor.spaces.active.use_proxies = ( cs_scene.contactsheet.contactsheet_meta.use_proxies ) # Remove contactsheet scene. bpy.data.scenes.remove(cs_scene) self.report({"INFO"}, f"Exited and deleted scene: {cs_scene_name}") return {"FINISHED"} # ----------------REGISTER--------------. classes = [ CS_OT_make_contactsheet, CS_OT_exit_contactsheet, ] def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)