2025-12-01
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user