io_scene_3ds: Update for Blender 3.x #2
217
storypencil/__init__.py
Normal file
217
storypencil/__init__.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# Define Addon info
|
||||||
|
# ----------------------------------------------
|
||||||
|
bl_info = {
|
||||||
|
"name": "Storypencil - Storyboard Tools",
|
||||||
|
"description": "Storyboard tools",
|
||||||
|
"author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas",
|
||||||
|
"version": (0, 1, 1),
|
||||||
|
"blender": (3, 3, 0),
|
||||||
|
"location": "",
|
||||||
|
"warning": "",
|
||||||
|
"category": "Sequencer",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# Import modules
|
||||||
|
# ----------------------------------------------
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(utils)
|
||||||
|
importlib.reload(synchro)
|
||||||
|
importlib.reload(dopesheet_overlay)
|
||||||
|
importlib.reload(scene_tools)
|
||||||
|
importlib.reload(render)
|
||||||
|
importlib.reload(ui)
|
||||||
|
else:
|
||||||
|
from . import utils
|
||||||
|
from . import synchro
|
||||||
|
from . import dopesheet_overlay
|
||||||
|
from . import scene_tools
|
||||||
|
from . import render
|
||||||
|
from . import ui
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import (
|
||||||
|
Scene,
|
||||||
|
WindowManager,
|
||||||
|
WorkSpace,
|
||||||
|
)
|
||||||
|
from bpy.props import (
|
||||||
|
BoolProperty,
|
||||||
|
IntProperty,
|
||||||
|
PointerProperty,
|
||||||
|
StringProperty,
|
||||||
|
EnumProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
# Register all operators, props and panels
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
classes = (
|
||||||
|
synchro.STORYPENCIL_PG_Settings,
|
||||||
|
scene_tools.STORYPENCIL_OT_Setup,
|
||||||
|
scene_tools.STORYPENCIL_OT_NewScene,
|
||||||
|
synchro.STORYPENCIL_OT_WindowBringFront,
|
||||||
|
synchro.STORYPENCIL_OT_WindowCloseOperator,
|
||||||
|
synchro.STORYPENCIL_OT_SyncToggleSlave,
|
||||||
|
synchro.STORYPENCIL_OT_SetSyncMainOperator,
|
||||||
|
synchro.STORYPENCIL_OT_AddSlaveWindowOperator,
|
||||||
|
synchro.STORYPENCIL_OT_Switch,
|
||||||
|
render.STORYPENCIL_OT_RenderAction,
|
||||||
|
ui.STORYPENCIL_PT_Settings,
|
||||||
|
ui.STORYPENCIL_PT_SettingsNew,
|
||||||
|
ui.STORYPENCIL_PT_RenderPanel,
|
||||||
|
ui.STORYPENCIL_PT_General,
|
||||||
|
ui.STORYPENCIL_MT_extra_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_mode(self, context):
|
||||||
|
wm = context.window_manager
|
||||||
|
wm['storypencil_use_new_window'] = context.scene.storypencil_use_new_window
|
||||||
|
# Close all secondary windows
|
||||||
|
if context.scene.storypencil_use_new_window is False:
|
||||||
|
c = context.copy()
|
||||||
|
for win in context.window_manager.windows:
|
||||||
|
# Don't close actual window
|
||||||
|
if win == context.window:
|
||||||
|
continue
|
||||||
|
win_id = str(win.as_pointer())
|
||||||
|
if win_id != wm.storypencil_settings.main_window_id and win.parent is None:
|
||||||
|
c["window"] = win
|
||||||
|
bpy.ops.wm.window_close(c)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
from bpy.utils import register_class
|
||||||
|
for cls in classes:
|
||||||
|
register_class(cls)
|
||||||
|
|
||||||
|
Scene.storypencil_scene_duration = IntProperty(
|
||||||
|
name="Scene Duration",
|
||||||
|
description="Default Duration for new Scene",
|
||||||
|
default=48,
|
||||||
|
min=1,
|
||||||
|
soft_max=250,
|
||||||
|
)
|
||||||
|
|
||||||
|
Scene.storypencil_use_new_window = BoolProperty(name="Open in new window",
|
||||||
|
description="Use secondary main window to edit scenes",
|
||||||
|
default=False,
|
||||||
|
update=save_mode)
|
||||||
|
|
||||||
|
Scene.storypencil_main_workspace = PointerProperty(type=WorkSpace,
|
||||||
|
description="Main Workspace used for editing Storyboard")
|
||||||
|
Scene.storypencil_main_scene = PointerProperty(type=Scene,
|
||||||
|
description="Main Scene used for editing Storyboard")
|
||||||
|
Scene.storypencil_edit_workspace = PointerProperty(type=WorkSpace,
|
||||||
|
description="Workspace used for changing drawings")
|
||||||
|
|
||||||
|
Scene.storypencil_base_scene = PointerProperty(type=Scene,
|
||||||
|
description="Base Scene used for creating new scenes")
|
||||||
|
|
||||||
|
Scene.storypencil_render_render_path = StringProperty(name="Output Path", subtype='FILE_PATH', maxlen=256,
|
||||||
|
description="Directory/name to save files")
|
||||||
|
|
||||||
|
Scene.storypencil_name_prefix = StringProperty(name="Scene Name Prefix", maxlen=20, default="")
|
||||||
|
|
||||||
|
Scene.storypencil_name_suffix = StringProperty(name="Scene Name Suffix", maxlen=20, default="")
|
||||||
|
|
||||||
|
Scene.storypencil_render_onlyselected = BoolProperty(name="Render only Selected Strips",
|
||||||
|
description="Render only the selected strips",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
Scene.storypencil_render_channel = IntProperty(name="Channel",
|
||||||
|
description="Channel to set the new rendered video",
|
||||||
|
default=5, min=1, max=128)
|
||||||
|
|
||||||
|
Scene.storypencil_add_render_strip = BoolProperty(name="Import Rendered Strips",
|
||||||
|
description="Add a Strip with the render",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
Scene.storypencil_render_step = IntProperty(name="Image Steps",
|
||||||
|
description="Minimum frames number to generate images between keyframes (0 to disable)",
|
||||||
|
default=0, min=0, max=128)
|
||||||
|
|
||||||
|
Scene.storypencil_render_numbering = EnumProperty(name="Image Numbering",
|
||||||
|
items=(
|
||||||
|
('1', "Frame", "Use real frame number"),
|
||||||
|
('2', "Consecutive", "Use sequential numbering"),
|
||||||
|
),
|
||||||
|
description="Defines how frame is named")
|
||||||
|
|
||||||
|
Scene.storypencil_add_render_byfolder = BoolProperty(name="Folder by Strip",
|
||||||
|
description="Create a separated folder for each strip",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
WindowManager.storypencil_settings = PointerProperty(
|
||||||
|
type=synchro.STORYPENCIL_PG_Settings,
|
||||||
|
name="Storypencil settings",
|
||||||
|
description="Storypencil tool settings",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Append Handlers
|
||||||
|
bpy.app.handlers.frame_change_post.clear()
|
||||||
|
bpy.app.handlers.frame_change_post.append(synchro.on_frame_changed)
|
||||||
|
bpy.app.handlers.load_post.append(synchro.sync_autoconfig)
|
||||||
|
|
||||||
|
bpy.context.window_manager.storypencil_settings.active = False
|
||||||
|
bpy.context.window_manager.storypencil_settings.main_window_id = ""
|
||||||
|
bpy.context.window_manager.storypencil_settings.slave_windows_ids = ""
|
||||||
|
|
||||||
|
# UI integration in dopesheet header
|
||||||
|
bpy.types.DOPESHEET_HT_header.append(synchro.draw_sync_header)
|
||||||
|
dopesheet_overlay.register()
|
||||||
|
|
||||||
|
synchro.sync_autoconfig()
|
||||||
|
|
||||||
|
# UI integration in VSE header
|
||||||
|
bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header)
|
||||||
|
bpy.types.SEQUENCER_HT_header.append(synchro.draw_sync_sequencer_header)
|
||||||
|
|
||||||
|
bpy.types.SEQUENCER_MT_add.append(scene_tools.draw_new_scene)
|
||||||
|
bpy.types.VIEW3D_MT_draw_gpencil.append(scene_tools.setup_storyboard)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
from bpy.utils import unregister_class
|
||||||
|
for cls in reversed(classes):
|
||||||
|
unregister_class(cls)
|
||||||
|
|
||||||
|
# Remove Handlers
|
||||||
|
if bpy.app.handlers.frame_change_post:
|
||||||
|
bpy.app.handlers.frame_change_post.remove(synchro.on_frame_changed)
|
||||||
|
bpy.app.handlers.load_post.remove(synchro.sync_autoconfig)
|
||||||
|
|
||||||
|
# remove UI integration
|
||||||
|
bpy.types.DOPESHEET_HT_header.remove(synchro.draw_sync_header)
|
||||||
|
dopesheet_overlay.unregister()
|
||||||
|
bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header)
|
||||||
|
|
||||||
|
bpy.types.SEQUENCER_MT_add.remove(scene_tools.draw_new_scene)
|
||||||
|
bpy.types.VIEW3D_MT_draw_gpencil.remove(scene_tools.setup_storyboard)
|
||||||
|
|
||||||
|
del Scene.storypencil_scene_duration
|
||||||
|
del WindowManager.storypencil_settings
|
||||||
|
|
||||||
|
del Scene.storypencil_base_scene
|
||||||
|
del Scene.storypencil_main_workspace
|
||||||
|
del Scene.storypencil_main_scene
|
||||||
|
del Scene.storypencil_edit_workspace
|
||||||
|
|
||||||
|
del Scene.storypencil_render_render_path
|
||||||
|
del Scene.storypencil_name_prefix
|
||||||
|
del Scene.storypencil_name_suffix
|
||||||
|
del Scene.storypencil_render_onlyselected
|
||||||
|
del Scene.storypencil_render_channel
|
||||||
|
del Scene.storypencil_render_step
|
||||||
|
del Scene.storypencil_add_render_strip
|
||||||
|
del Scene.storypencil_render_numbering
|
||||||
|
del Scene.storypencil_add_render_byfolder
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
register()
|
180
storypencil/dopesheet_overlay.py
Normal file
180
storypencil/dopesheet_overlay.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import bgl
|
||||||
|
import gpu
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
|
||||||
|
from .utils import (redraw_all_areas_by_type)
|
||||||
|
from .synchro import (is_slave_window, window_id, get_main_strip)
|
||||||
|
|
||||||
|
Int3 = typing.Tuple[int, int, int]
|
||||||
|
|
||||||
|
Float2 = typing.Tuple[float, float]
|
||||||
|
Float3 = typing.Tuple[float, float, float]
|
||||||
|
Float4 = typing.Tuple[float, float, float, float]
|
||||||
|
|
||||||
|
|
||||||
|
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.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||||
|
|
||||||
|
def draw(
|
||||||
|
self,
|
||||||
|
coords: typing.List[Float2],
|
||||||
|
indices: typing.List[Int3],
|
||||||
|
color: Float4,
|
||||||
|
):
|
||||||
|
if not coords:
|
||||||
|
return
|
||||||
|
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
|
||||||
|
self.shader.uniform_float("color", color)
|
||||||
|
|
||||||
|
batch = batch_for_shader(self.shader, 'TRIS', {"pos": coords}, indices=indices)
|
||||||
|
batch.program_set(self.shader)
|
||||||
|
batch.draw()
|
||||||
|
|
||||||
|
bgl.glDisable(bgl.GL_BLEND)
|
||||||
|
|
||||||
|
|
||||||
|
def get_scene_strip_in_out(strip):
|
||||||
|
""" Return the in and out keyframe of the given strip in the scene time reference"""
|
||||||
|
shot_in = strip.scene.frame_start + strip.frame_offset_start
|
||||||
|
shot_out = shot_in + strip.frame_final_duration - 1
|
||||||
|
return (shot_in, shot_out)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_callback_px(line_drawer: LineDrawer):
|
||||||
|
context = bpy.context
|
||||||
|
region = context.region
|
||||||
|
|
||||||
|
wm = context.window_manager
|
||||||
|
|
||||||
|
if (
|
||||||
|
not wm.storypencil_settings.active
|
||||||
|
or not wm.storypencil_settings.show_main_strip_range
|
||||||
|
or not is_slave_window(wm, window_id(context.window))
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# get main strip driving the sync
|
||||||
|
strip = get_main_strip(wm)
|
||||||
|
|
||||||
|
if not strip or strip.scene != context.scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
|
||||||
|
one_pixel_further_x = region.view2d.region_to_view(1, 1)[0]
|
||||||
|
pixel_size_x = one_pixel_further_x - xwin1
|
||||||
|
rect_width = 1
|
||||||
|
|
||||||
|
shot_in, shot_out = get_scene_strip_in_out(strip)
|
||||||
|
key_coords_in = [
|
||||||
|
(
|
||||||
|
shot_in - rect_width * pixel_size_x,
|
||||||
|
ywin1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_in + rect_width * pixel_size_x,
|
||||||
|
ywin1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_in + rect_width * pixel_size_x,
|
||||||
|
ywin1 + context.region.height,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_in - rect_width * pixel_size_x,
|
||||||
|
ywin1 + context.region.height,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
key_coords_out = [
|
||||||
|
(
|
||||||
|
shot_out - rect_width * pixel_size_x,
|
||||||
|
ywin1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_out + rect_width * pixel_size_x,
|
||||||
|
ywin1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_out + rect_width * pixel_size_x,
|
||||||
|
ywin1 + context.region.height,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
shot_out - rect_width * pixel_size_x,
|
||||||
|
ywin1 + context.region.height,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
indices = [(0, 1, 2), (2, 0, 3)]
|
||||||
|
# Draw the IN frame in green
|
||||||
|
# hack: in certain cases, opengl draw state is invalid for the first drawn item
|
||||||
|
# resulting in a non-colored line
|
||||||
|
# => draw it a first time with a null alpha, so that the second one is drawn correctly
|
||||||
|
line_drawer.draw(key_coords_in, indices, (0, 0, 0, 0))
|
||||||
|
line_drawer.draw(key_coords_in, indices, (0.3, 0.99, 0.4, 0.5))
|
||||||
|
# Draw the OUT frame un red
|
||||||
|
line_drawer.draw(key_coords_out, indices, (0.99, 0.3, 0.4, 0.5))
|
||||||
|
|
||||||
|
|
||||||
|
def tag_redraw_all_dopesheets():
|
||||||
|
redraw_all_areas_by_type(bpy.context, 'DOPESHEET')
|
||||||
|
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
if cb_handle:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Doing GPU stuff in the background crashes Blender, so let's not.
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
line_drawer = LineDrawer()
|
||||||
|
# POST_VIEW allow to work in time coordinate (1 unit = 1 frame)
|
||||||
|
cb_handle[:] = (
|
||||||
|
bpy.types.SpaceDopeSheetEditor.draw_handler_add(
|
||||||
|
draw_callback_px, (line_drawer,), 'WINDOW', 'POST_VIEW'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tag_redraw_all_dopesheets()
|
||||||
|
|
||||||
|
|
||||||
|
def callback_disable():
|
||||||
|
if not cb_handle:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
bpy.types.SpaceDopeSheetEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
|
||||||
|
except ValueError:
|
||||||
|
# Thrown when already removed.
|
||||||
|
pass
|
||||||
|
cb_handle.clear()
|
||||||
|
|
||||||
|
tag_redraw_all_dopesheets()
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
callback_enable()
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
callback_disable()
|
281
storypencil/render.py
Normal file
281
storypencil/render.py
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from bpy.types import Operator
|
||||||
|
from .utils import get_keyframe_list
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Button: Render VSE
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_RenderAction(Operator):
|
||||||
|
bl_idname = "storypencil.render_vse"
|
||||||
|
bl_label = "Render Strips"
|
||||||
|
bl_description = "Render VSE strips"
|
||||||
|
|
||||||
|
# Extension by FFMPEG container type
|
||||||
|
video_ext = {
|
||||||
|
"MPEG1": ".mpg",
|
||||||
|
"MPEG2": ".dvd",
|
||||||
|
"MPEG4": ".mp4",
|
||||||
|
"AVI": ".avi",
|
||||||
|
"QUICKTIME": ".mov",
|
||||||
|
"DV": ".dv",
|
||||||
|
"OGG": ".ogv",
|
||||||
|
"MKV": ".mkv",
|
||||||
|
"FLASH": ".flv",
|
||||||
|
"WEBM": ".webm"
|
||||||
|
}
|
||||||
|
# Extension by image format
|
||||||
|
image_ext = {
|
||||||
|
"BMP": ".bmp",
|
||||||
|
"IRIS": ".rgb",
|
||||||
|
"PNG": ".png",
|
||||||
|
"JPEG": ".jpg",
|
||||||
|
"JPEG2000": ".jp2",
|
||||||
|
"TARGA": ".tga",
|
||||||
|
"TARGA_RAW": ".tga",
|
||||||
|
"CINEON": ".cin",
|
||||||
|
"DPX": ".dpx",
|
||||||
|
"OPEN_EXR_MULTILAYER": ".exr",
|
||||||
|
"OPEN_EXR": ".exr",
|
||||||
|
"HDR": ".hdr",
|
||||||
|
"TIFF": ".tif",
|
||||||
|
"WEBP": ".webp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Format an int adding 4 zero padding
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
def format_to4(self, value):
|
||||||
|
return f"{value:04}"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Add frames every N frames
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
def add_missing_frames(self, sq, step, keyframe_list):
|
||||||
|
missing = []
|
||||||
|
lk = len(keyframe_list)
|
||||||
|
if lk == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add mid frames
|
||||||
|
if step > 0:
|
||||||
|
for i in range(0, lk - 1):
|
||||||
|
dist = keyframe_list[i + 1] - keyframe_list[i]
|
||||||
|
if dist > step:
|
||||||
|
delta = int(dist / step)
|
||||||
|
e = 1
|
||||||
|
for x in range(1, delta):
|
||||||
|
missing.append(keyframe_list[i] + (step * e))
|
||||||
|
e += 1
|
||||||
|
|
||||||
|
keyframe_list.extend(missing)
|
||||||
|
keyframe_list.sort()
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Execute
|
||||||
|
# ------------------------------
|
||||||
|
def execute(self, context):
|
||||||
|
scene = bpy.context.scene
|
||||||
|
image_settings = scene.render.image_settings
|
||||||
|
is_video_output = image_settings.file_format in {
|
||||||
|
'FFMPEG', 'AVI_JPEG', 'AVI_RAW'}
|
||||||
|
step = scene.storypencil_render_step
|
||||||
|
|
||||||
|
sequences = scene.sequence_editor.sequences_all
|
||||||
|
prv_start = scene.frame_start
|
||||||
|
prv_end = scene.frame_end
|
||||||
|
prv_frame = bpy.context.scene.frame_current
|
||||||
|
|
||||||
|
prv_path = scene.render.filepath
|
||||||
|
prv_format = image_settings.file_format
|
||||||
|
prv_use_file_extension = scene.render.use_file_extension
|
||||||
|
prv_ffmpeg_format = scene.render.ffmpeg.format
|
||||||
|
rootpath = scene.storypencil_render_render_path
|
||||||
|
only_selected = scene.storypencil_render_onlyselected
|
||||||
|
channel = scene.storypencil_render_channel
|
||||||
|
|
||||||
|
context.window.cursor_set('WAIT')
|
||||||
|
|
||||||
|
# Create list of selected strips because the selection is changed when adding new strips
|
||||||
|
Strips = []
|
||||||
|
for sq in sequences:
|
||||||
|
if sq.type == 'SCENE':
|
||||||
|
if only_selected is False or sq.select is True:
|
||||||
|
Strips.append(sq)
|
||||||
|
|
||||||
|
# Sort strips
|
||||||
|
Strips = sorted(Strips, key=lambda strip: strip.frame_start)
|
||||||
|
|
||||||
|
# For video, clear BL_proxy folder because sometimes the video
|
||||||
|
# is not rendered as expected if this folder has data.
|
||||||
|
# This ensure the output video is correct.
|
||||||
|
if is_video_output:
|
||||||
|
proxy_folder = os.path.join(rootpath, "BL_proxy")
|
||||||
|
if os.path.exists(proxy_folder):
|
||||||
|
for filename in os.listdir(proxy_folder):
|
||||||
|
file_path = os.path.join(proxy_folder, filename)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
elif os.path.isdir(file_path):
|
||||||
|
shutil.rmtree(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to delete %s. Reason: %s' %
|
||||||
|
(file_path, e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
Videos = []
|
||||||
|
Sheets = []
|
||||||
|
# Read all strips and render the output
|
||||||
|
for sq in Strips:
|
||||||
|
strip_name = sq.name
|
||||||
|
strip_scene = sq.scene
|
||||||
|
scene.frame_start = int(sq.frame_start + sq.frame_offset_start)
|
||||||
|
scene.frame_end = int(scene.frame_start + sq.frame_final_duration - 1) # Image
|
||||||
|
if is_video_output is False:
|
||||||
|
# Get list of any keyframe
|
||||||
|
strip_start = sq.frame_offset_start
|
||||||
|
if strip_start < strip_scene.frame_start:
|
||||||
|
strip_start = strip_scene.frame_start
|
||||||
|
|
||||||
|
strip_end = strip_start + sq.frame_final_duration - 1
|
||||||
|
keyframe_list = get_keyframe_list(
|
||||||
|
strip_scene, strip_start, strip_end)
|
||||||
|
self.add_missing_frames(sq, step, keyframe_list)
|
||||||
|
|
||||||
|
scene.render.use_file_extension = True
|
||||||
|
foldername = strip_name
|
||||||
|
if scene.storypencil_add_render_byfolder is True:
|
||||||
|
root_folder = os.path.join(rootpath, foldername)
|
||||||
|
else:
|
||||||
|
root_folder = rootpath
|
||||||
|
|
||||||
|
frame_nrr = 0
|
||||||
|
print("Render:" + strip_name + "/" + strip_scene.name)
|
||||||
|
print("Image From:", strip_start, "To", strip_end)
|
||||||
|
for key in range(int(strip_start), int(strip_end) + 1):
|
||||||
|
if key not in keyframe_list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
keyframe = key + sq.frame_start
|
||||||
|
if scene.use_preview_range:
|
||||||
|
if keyframe < scene.frame_preview_start:
|
||||||
|
continue
|
||||||
|
if keyframe > scene.frame_preview_end:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if keyframe < scene.frame_start:
|
||||||
|
continue
|
||||||
|
if keyframe > scene.frame_end:
|
||||||
|
break
|
||||||
|
# For frame name use only the number
|
||||||
|
if scene.storypencil_render_numbering == '1':
|
||||||
|
# Real
|
||||||
|
framename = strip_name + '.' + self.format_to4(key)
|
||||||
|
else:
|
||||||
|
# Consecutive
|
||||||
|
frame_nrr += 1
|
||||||
|
framename = strip_name + '.' + \
|
||||||
|
self.format_to4(frame_nrr)
|
||||||
|
|
||||||
|
filepath = os.path.join(root_folder, framename)
|
||||||
|
|
||||||
|
sheet = os.path.realpath(filepath)
|
||||||
|
sheet = bpy.path.ensure_ext(
|
||||||
|
sheet, self.image_ext[image_settings.file_format])
|
||||||
|
Sheets.append([sheet, keyframe])
|
||||||
|
|
||||||
|
scene.render.filepath = filepath
|
||||||
|
|
||||||
|
# Render Frame
|
||||||
|
scene.frame_set(int(keyframe - 1.0), subframe=0.0)
|
||||||
|
bpy.ops.render.render(
|
||||||
|
animation=False, write_still=True)
|
||||||
|
|
||||||
|
# Add strip with the corresponding length
|
||||||
|
if scene.storypencil_add_render_strip:
|
||||||
|
frame_start = sq.frame_start + key - 1
|
||||||
|
index = keyframe_list.index(key)
|
||||||
|
if index < len(keyframe_list) - 1:
|
||||||
|
key_next = keyframe_list[index + 1]
|
||||||
|
frame_end = frame_start + (key_next - key)
|
||||||
|
else:
|
||||||
|
frame_end = scene.frame_end + 1
|
||||||
|
|
||||||
|
if index == 0 and frame_start > scene.frame_start:
|
||||||
|
frame_start = scene.frame_start
|
||||||
|
|
||||||
|
if frame_end < frame_start:
|
||||||
|
frame_end = frame_start
|
||||||
|
image_ext = self.image_ext[image_settings.file_format]
|
||||||
|
bpy.ops.sequencer.image_strip_add(directory=root_folder,
|
||||||
|
files=[
|
||||||
|
{"name": framename + image_ext}],
|
||||||
|
frame_start=int(frame_start),
|
||||||
|
frame_end=int(frame_end),
|
||||||
|
channel=channel)
|
||||||
|
else:
|
||||||
|
print("Render:" + strip_name + "/" + strip_scene.name)
|
||||||
|
print("Video From:", scene.frame_start,
|
||||||
|
"To", scene.frame_end)
|
||||||
|
# Video
|
||||||
|
filepath = os.path.join(rootpath, strip_name)
|
||||||
|
|
||||||
|
if image_settings.file_format == 'FFMPEG':
|
||||||
|
ext = self.video_ext[scene.render.ffmpeg.format]
|
||||||
|
else:
|
||||||
|
ext = '.avi'
|
||||||
|
|
||||||
|
if not filepath.endswith(ext):
|
||||||
|
filepath += ext
|
||||||
|
|
||||||
|
scene.render.use_file_extension = False
|
||||||
|
scene.render.filepath = filepath
|
||||||
|
|
||||||
|
# Render Animation
|
||||||
|
bpy.ops.render.render(animation=True)
|
||||||
|
|
||||||
|
# Add video to add strip later
|
||||||
|
if scene.storypencil_add_render_strip:
|
||||||
|
Videos.append(
|
||||||
|
[filepath, sq.frame_start + sq.frame_offset_start])
|
||||||
|
|
||||||
|
# Add pending video Strips
|
||||||
|
for vid in Videos:
|
||||||
|
bpy.ops.sequencer.movie_strip_add(filepath=vid[0],
|
||||||
|
frame_start=int(vid[1]),
|
||||||
|
channel=channel)
|
||||||
|
|
||||||
|
scene.frame_start = prv_start
|
||||||
|
scene.frame_end = prv_end
|
||||||
|
scene.render.use_file_extension = prv_use_file_extension
|
||||||
|
image_settings.file_format = prv_format
|
||||||
|
scene.render.ffmpeg.format = prv_ffmpeg_format
|
||||||
|
|
||||||
|
scene.render.filepath = prv_path
|
||||||
|
scene.frame_set(int(prv_frame))
|
||||||
|
|
||||||
|
context.window.cursor_set('DEFAULT')
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("Unexpected error:" + str(sys.exc_info()))
|
||||||
|
self.report({'ERROR'}, "Unable to render")
|
||||||
|
scene.frame_start = prv_start
|
||||||
|
scene.frame_end = prv_end
|
||||||
|
scene.render.use_file_extension = prv_use_file_extension
|
||||||
|
image_settings.file_format = prv_format
|
||||||
|
|
||||||
|
scene.render.filepath = prv_path
|
||||||
|
scene.frame_set(int(prv_frame))
|
||||||
|
context.window.cursor_set('DEFAULT')
|
||||||
|
return {'FINISHED'}
|
173
storypencil/scene_tools.py
Normal file
173
storypencil/scene_tools.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bpy.types import (
|
||||||
|
Operator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Add a new scene and set to new strip
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
class STORYPENCIL_OT_NewScene(Operator):
|
||||||
|
bl_idname = "storypencil.new_scene"
|
||||||
|
bl_label = "New Scene"
|
||||||
|
bl_description = "Create a new scene base on template scene"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
scene_name: bpy.props.StringProperty(default="Scene")
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Poll
|
||||||
|
# ------------------------------
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
scene = context.scene
|
||||||
|
scene_base = scene.storypencil_base_scene
|
||||||
|
if scene_base is not None and scene_base.name in bpy.data.scenes:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
col = layout.column()
|
||||||
|
col.prop(self, "scene_name", text="Scene Name")
|
||||||
|
|
||||||
|
def format_to3(self, value):
|
||||||
|
return f"{value:03}"
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Execute button action
|
||||||
|
# ------------------------------
|
||||||
|
def execute(self, context):
|
||||||
|
scene_prv = context.scene
|
||||||
|
cfra_prv = scene_prv.frame_current
|
||||||
|
scene_base = scene_prv.storypencil_base_scene
|
||||||
|
|
||||||
|
# Set context to base scene and duplicate
|
||||||
|
context.window.scene = scene_base
|
||||||
|
bpy.ops.scene.new(type='FULL_COPY')
|
||||||
|
scene_new = context.window.scene
|
||||||
|
new_name = scene_prv.storypencil_name_prefix + \
|
||||||
|
self.scene_name + scene_prv.storypencil_name_suffix
|
||||||
|
id = 0
|
||||||
|
while new_name in bpy.data.scenes:
|
||||||
|
id += 1
|
||||||
|
new_name = scene_prv.storypencil_name_prefix + self.scene_name + \
|
||||||
|
scene_prv.storypencil_name_suffix + '.' + self.format_to3(id)
|
||||||
|
|
||||||
|
scene_new.name = new_name
|
||||||
|
# Set duration of new scene
|
||||||
|
scene_new.frame_end = scene_new.frame_start + \
|
||||||
|
scene_prv.storypencil_scene_duration - 1
|
||||||
|
|
||||||
|
# Back to original scene
|
||||||
|
context.window.scene = scene_prv
|
||||||
|
scene_prv.frame_current = cfra_prv
|
||||||
|
bpy.ops.sequencer.scene_strip_add(
|
||||||
|
frame_start=cfra_prv, scene=scene_new.name)
|
||||||
|
|
||||||
|
scene_new.update_tag()
|
||||||
|
scene_prv.update_tag()
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def draw_new_scene(self, context):
|
||||||
|
"""Add menu options."""
|
||||||
|
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN'
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Base Scene")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_storyboard(self, context):
|
||||||
|
"""Add Setup menu option."""
|
||||||
|
# For security, check if this is the default template.
|
||||||
|
is_gpencil = context.active_object and context.active_object.name == 'Stroke'
|
||||||
|
if is_gpencil and context.workspace.name in ('2D Animation', '2D Full Canvas') and context.scene.name == 'Scene':
|
||||||
|
if "Video Editing" not in bpy.data.workspaces:
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.separator()
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.operator(STORYPENCIL_OT_Setup.bl_idname,
|
||||||
|
text="Setup Storyboard Session")
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Setup all environment
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
class STORYPENCIL_OT_Setup(Operator):
|
||||||
|
bl_idname = "storypencil.setup"
|
||||||
|
bl_label = "Setup"
|
||||||
|
bl_description = "Configure all settings for a storyboard session"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Poll
|
||||||
|
# ------------------------------
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_workspace(self, type):
|
||||||
|
for wrk in bpy.data.workspaces:
|
||||||
|
if wrk.name == type:
|
||||||
|
return wrk
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Execute button action
|
||||||
|
# ------------------------------
|
||||||
|
def execute(self, context):
|
||||||
|
scene_base = context.scene
|
||||||
|
# Create Workspace
|
||||||
|
templatepath = None
|
||||||
|
if "Video Editing" not in bpy.data.workspaces:
|
||||||
|
template_path = None
|
||||||
|
for path in bpy.utils.app_template_paths():
|
||||||
|
template_path = path
|
||||||
|
|
||||||
|
filepath = os.path.join(
|
||||||
|
template_path, "Video_Editing", "startup.blend")
|
||||||
|
bpy.ops.workspace.append_activate(
|
||||||
|
idname="Video Editing", filepath=filepath)
|
||||||
|
# Create New scene
|
||||||
|
bpy.ops.scene.new()
|
||||||
|
scene_edit = context.scene
|
||||||
|
scene_edit.name = 'Edit'
|
||||||
|
# Rename original base scene
|
||||||
|
scene_base.name = 'Base'
|
||||||
|
# Setup Edit scene settings
|
||||||
|
scene_edit.storypencil_main_workspace = self.get_workspace(
|
||||||
|
"Video Editing")
|
||||||
|
scene_edit.storypencil_main_scene = scene_edit
|
||||||
|
scene_edit.storypencil_base_scene = scene_base
|
||||||
|
scene_edit.storypencil_edit_workspace = self.get_workspace(
|
||||||
|
"2D Animation")
|
||||||
|
|
||||||
|
# Add a new strip (need set the area context)
|
||||||
|
context.window.scene = scene_edit
|
||||||
|
area_prv = context.area.ui_type
|
||||||
|
context.area.ui_type = 'SEQUENCE_EDITOR'
|
||||||
|
prv_frame = scene_edit.frame_current
|
||||||
|
|
||||||
|
scene_edit.frame_current = scene_edit.frame_start
|
||||||
|
bpy.ops.storypencil.new_scene()
|
||||||
|
|
||||||
|
context.area.ui_type = area_prv
|
||||||
|
scene_edit.frame_current = prv_frame
|
||||||
|
|
||||||
|
scene_edit.update_tag()
|
||||||
|
bpy.ops.sequencer.reload()
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
790
storypencil/synchro.py
Normal file
790
storypencil/synchro.py
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from typing import List, Sequence, Tuple
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
|
from bpy.types import (
|
||||||
|
Context,
|
||||||
|
MetaSequence,
|
||||||
|
Operator,
|
||||||
|
PropertyGroup,
|
||||||
|
SceneSequence,
|
||||||
|
Window,
|
||||||
|
WindowManager,
|
||||||
|
)
|
||||||
|
from bpy.props import (
|
||||||
|
BoolProperty,
|
||||||
|
IntProperty,
|
||||||
|
StringProperty,
|
||||||
|
)
|
||||||
|
from .scene_tools import STORYPENCIL_OT_NewScene
|
||||||
|
from .render import STORYPENCIL_OT_RenderAction
|
||||||
|
|
||||||
|
def window_id(window: Window) -> str:
|
||||||
|
""" Get Window's ID.
|
||||||
|
|
||||||
|
:param window: the Window to consider
|
||||||
|
:return: the Window's ID
|
||||||
|
"""
|
||||||
|
return str(window.as_pointer())
|
||||||
|
|
||||||
|
|
||||||
|
def get_window_from_id(wm: WindowManager, win_id: str) -> Window:
|
||||||
|
"""Get a Window object from its ID (serialized ptr).
|
||||||
|
|
||||||
|
:param wm: a WindowManager holding Windows
|
||||||
|
:param win_id: the ID of the Window to get
|
||||||
|
:return: the Window matching the given ID, None otherwise
|
||||||
|
"""
|
||||||
|
return next((w for w in wm.windows if w and window_id(w) == win_id), None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_main_windows_list(wm: WindowManager) -> Sequence[Window]:
|
||||||
|
"""Get all the Main Windows held by the given WindowManager `wm`"""
|
||||||
|
return [w for w in wm.windows if w and w.parent is None]
|
||||||
|
|
||||||
|
|
||||||
|
def join_win_ids(ids: List[str]) -> str:
|
||||||
|
"""Join Windows IDs in a single string"""
|
||||||
|
return ";".join(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def split_win_ids(ids: str) -> List[str]:
|
||||||
|
"""Split a Windows IDs string into individual IDs"""
|
||||||
|
return ids.split(";")
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_SetSyncMainOperator(Operator):
|
||||||
|
bl_idname = "storypencil.sync_set_main"
|
||||||
|
bl_label = "Set as Sync Main"
|
||||||
|
bl_description = "Set this Window as main for Synchronization"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
win_id: bpy.props.StringProperty(
|
||||||
|
name="Window ID",
|
||||||
|
default="",
|
||||||
|
options=set(),
|
||||||
|
description="Main window ID",
|
||||||
|
)
|
||||||
|
|
||||||
|
def copy_settings(self, main_window, slave_window):
|
||||||
|
if main_window is None or slave_window is None:
|
||||||
|
return
|
||||||
|
slave_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace
|
||||||
|
slave_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene
|
||||||
|
slave_window.scene.storypencil_edit_workspace = main_window.scene.storypencil_edit_workspace
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
options = context.window_manager.storypencil_settings
|
||||||
|
options.main_window_id = self.win_id
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
scene = context.scene
|
||||||
|
wm['storypencil_use_new_window'] = scene.storypencil_use_new_window
|
||||||
|
|
||||||
|
main_windows = get_main_windows_list(wm)
|
||||||
|
main_window = get_main_window(wm)
|
||||||
|
slave_window = get_slave_window(wm)
|
||||||
|
# Active sync
|
||||||
|
options.active = True
|
||||||
|
if slave_window is None:
|
||||||
|
# Open a new window
|
||||||
|
if len(main_windows) < 2:
|
||||||
|
bpy.ops.storypencil.create_slave_window()
|
||||||
|
slave_window = get_slave_window(wm)
|
||||||
|
self.copy_settings(get_main_window(wm), slave_window)
|
||||||
|
return {'FINISHED'}
|
||||||
|
else:
|
||||||
|
# Reuse the existing window
|
||||||
|
slave_window = get_not_main_window(wm)
|
||||||
|
else:
|
||||||
|
# Open new slave
|
||||||
|
if len(main_windows) < 2:
|
||||||
|
bpy.ops.storypencil.create_slave_window()
|
||||||
|
slave_window = get_slave_window(wm)
|
||||||
|
self.copy_settings(get_main_window(wm), slave_window)
|
||||||
|
return {'FINISHED'}
|
||||||
|
else:
|
||||||
|
# Reuse the existing window
|
||||||
|
slave_window = get_not_main_window(wm)
|
||||||
|
|
||||||
|
if slave_window:
|
||||||
|
enable_slave_window(wm, window_id(slave_window))
|
||||||
|
win_id = window_id(slave_window)
|
||||||
|
self.copy_settings(get_main_window(wm), slave_window)
|
||||||
|
bpy.ops.storypencil.sync_window_bring_front(win_id=win_id)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_AddSlaveWindowOperator(Operator):
|
||||||
|
bl_idname = "storypencil.create_slave_window"
|
||||||
|
bl_label = "Create Slave Window"
|
||||||
|
bl_description = "Create a Slave Main Window and enable Synchronization"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# store existing windows
|
||||||
|
windows = set(context.window_manager.windows[:])
|
||||||
|
bpy.ops.wm.window_new_main()
|
||||||
|
# get newly created window by comparing to previous list
|
||||||
|
new_window = (set(context.window_manager.windows[:]) - windows).pop()
|
||||||
|
# activate sync system and enable sync for this window
|
||||||
|
toggle_slave_window(context.window_manager, window_id(new_window))
|
||||||
|
context.window_manager.storypencil_settings.active = True
|
||||||
|
# trigger initial synchronization to open the current Sequence's Scene
|
||||||
|
on_frame_changed()
|
||||||
|
# Configure the new window
|
||||||
|
self.configure_new_slave_window(context, new_window)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def configure_new_slave_window(self, context, new_window):
|
||||||
|
wrk_name = context.scene.storypencil_edit_workspace.name
|
||||||
|
override_context = context.copy()
|
||||||
|
override_context["window"] = new_window
|
||||||
|
# Open the 2D workspace
|
||||||
|
blendpath = os.path.dirname(bpy.app.binary_path)
|
||||||
|
version = bpy.app.version
|
||||||
|
version_full = str(version[0]) + '.' + str(version[1])
|
||||||
|
template = os.path.join("scripts", "startup",
|
||||||
|
"bl_app_templates_system")
|
||||||
|
template = os.path.join(template, wrk_name, "startup.blend")
|
||||||
|
template_path = os.path.join(blendpath, version_full, template)
|
||||||
|
# Check if workspace exist and add it if missing
|
||||||
|
for wk in bpy.data.workspaces:
|
||||||
|
if wk.name == wrk_name:
|
||||||
|
new_window.workspace = wk
|
||||||
|
return
|
||||||
|
bpy.ops.workspace.append_activate(
|
||||||
|
override_context, idname=wk_name, filepath=template_path)
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_WindowBringFront(Operator):
|
||||||
|
bl_idname = "storypencil.sync_window_bring_front"
|
||||||
|
bl_label = "Bring Window Front"
|
||||||
|
bl_description = "Bring a Window to Front"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
win_id: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
c = context.copy()
|
||||||
|
win = get_window_from_id(context.window_manager, self.win_id)
|
||||||
|
if not win:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
c["window"] = win
|
||||||
|
bpy.ops.wm.window_fullscreen_toggle(c)
|
||||||
|
bpy.ops.wm.window_fullscreen_toggle(c)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_WindowCloseOperator(Operator):
|
||||||
|
bl_idname = "storypencil.close_slave_window"
|
||||||
|
bl_label = "Close Window"
|
||||||
|
bl_description = "Close a specific Window"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
win_id: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
c = context.copy()
|
||||||
|
win = get_window_from_id(context.window_manager, self.win_id)
|
||||||
|
if not win:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
c["window"] = win
|
||||||
|
bpy.ops.wm.window_close(c)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sync(window_manager: WindowManager) -> bool:
|
||||||
|
"""
|
||||||
|
Ensure synchronization system is functional, with a valid main window.
|
||||||
|
Disable it otherwise and return the system status.
|
||||||
|
"""
|
||||||
|
if not window_manager.storypencil_settings.active:
|
||||||
|
return False
|
||||||
|
if not get_window_from_id(window_manager, window_manager.storypencil_settings.main_window_id):
|
||||||
|
window_manager.storypencil_settings.active = False
|
||||||
|
return window_manager.storypencil_settings.active
|
||||||
|
|
||||||
|
|
||||||
|
def get_slave_window_indices(wm: WindowManager) -> List[str]:
|
||||||
|
"""Get slave Windows indices as a list of IDs
|
||||||
|
|
||||||
|
:param wm: the WindowManager to consider
|
||||||
|
:return: the list of slave Windows IDs
|
||||||
|
"""
|
||||||
|
return split_win_ids(wm.storypencil_settings.slave_windows_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def is_slave_window(window_manager: WindowManager, win_id: str) -> bool:
|
||||||
|
"""Return wether the Window identified by 'win_id' is a slave window.
|
||||||
|
|
||||||
|
:return: whether this Window is a sync slave
|
||||||
|
"""
|
||||||
|
return win_id in get_slave_window_indices(window_manager)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_slave_window(wm: WindowManager, win_id: str):
|
||||||
|
"""Enable the slave status of a Window.
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:param win_id: the id of the window
|
||||||
|
"""
|
||||||
|
slave_indices = get_slave_window_indices(wm)
|
||||||
|
win_id_str = win_id
|
||||||
|
# Delete old indice if exist
|
||||||
|
if win_id_str in slave_indices:
|
||||||
|
slave_indices.remove(win_id_str)
|
||||||
|
|
||||||
|
# Add indice
|
||||||
|
slave_indices.append(win_id_str)
|
||||||
|
|
||||||
|
# rebuild the whole list of valid slave windows
|
||||||
|
slave_indices = [
|
||||||
|
idx for idx in slave_indices if get_window_from_id(wm, idx)]
|
||||||
|
|
||||||
|
wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_slave_window(wm: WindowManager, win_id: str):
|
||||||
|
"""Toggle the slave status of a Window.
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:param win_id: the id of the window
|
||||||
|
"""
|
||||||
|
slave_indices = get_slave_window_indices(wm)
|
||||||
|
win_id_str = win_id
|
||||||
|
if win_id_str in slave_indices:
|
||||||
|
slave_indices.remove(win_id_str)
|
||||||
|
else:
|
||||||
|
slave_indices.append(win_id_str)
|
||||||
|
|
||||||
|
# rebuild the whole list of valid slave windows
|
||||||
|
slave_indices = [
|
||||||
|
idx for idx in slave_indices if get_window_from_id(wm, idx)]
|
||||||
|
|
||||||
|
wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices)
|
||||||
|
|
||||||
|
|
||||||
|
def get_main_window(wm: WindowManager) -> Window:
|
||||||
|
"""Get the Window used to drive the synchronization system
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:returns: the main Window or None
|
||||||
|
"""
|
||||||
|
return get_window_from_id(wm=wm, win_id=wm.storypencil_settings.main_window_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slave_window(wm: WindowManager) -> Window:
|
||||||
|
"""Get the first slave Window
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:returns: the Window or None
|
||||||
|
"""
|
||||||
|
for w in wm.windows:
|
||||||
|
win_id = window_id(w)
|
||||||
|
if is_slave_window(wm, win_id):
|
||||||
|
return w
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_not_main_window(wm: WindowManager) -> Window:
|
||||||
|
"""Get the first not main Window
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:returns: the Window or None
|
||||||
|
"""
|
||||||
|
for w in wm.windows:
|
||||||
|
win_id = window_id(w)
|
||||||
|
if win_id != wm.storypencil_settings.main_window_id:
|
||||||
|
return w
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_main_strip(wm: WindowManager) -> SceneSequence:
|
||||||
|
"""Get Scene Strip at current time in Main window
|
||||||
|
|
||||||
|
:param wm: the WindowManager instance
|
||||||
|
:returns: the Strip at current time or None
|
||||||
|
"""
|
||||||
|
main_window = get_main_window(wm=wm)
|
||||||
|
if not main_window or not main_window.scene.sequence_editor:
|
||||||
|
return None
|
||||||
|
seq_editor = main_window.scene.sequence_editor
|
||||||
|
return seq_editor.sequences.get(wm.storypencil_settings.main_strip_name, None)
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_OT_SyncToggleSlave(Operator):
|
||||||
|
bl_idname = "storypencil.sync_toggle_slave"
|
||||||
|
bl_label = "Toggle Slave Window Status"
|
||||||
|
bl_description = "Enable/Disable synchronization for a specific Window"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
win_id: bpy.props.StringProperty(name="Window Index")
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
wm = context.window_manager
|
||||||
|
toggle_slave_window(wm, self.win_id)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
def get_sequences_at_frame(
|
||||||
|
frame: int,
|
||||||
|
sequences: Sequence[Sequence]) -> Sequence[bpy.types.Sequence]:
|
||||||
|
""" Get all sequencer strips at given frame.
|
||||||
|
|
||||||
|
:param frame: the frame to consider
|
||||||
|
"""
|
||||||
|
return [s for s in sequences if frame >= s.frame_start + s.frame_offset_start and
|
||||||
|
frame < s.frame_start + s.frame_offset_start + s.frame_final_duration]
|
||||||
|
|
||||||
|
|
||||||
|
def get_sequence_at_frame(
|
||||||
|
frame: int,
|
||||||
|
sequences: Sequence[bpy.types.Sequence] = None,
|
||||||
|
skip_muted: bool = True,
|
||||||
|
) -> Tuple[bpy.types.Sequence, int]:
|
||||||
|
"""
|
||||||
|
Get the higher sequence strip in channels stack at current frame.
|
||||||
|
Recursively enters scene sequences and returns the original frame in the
|
||||||
|
returned strip's time referential.
|
||||||
|
|
||||||
|
:param frame: the frame to consider
|
||||||
|
:param skip_muted: skip muted strips
|
||||||
|
:returns: the sequence strip and the frame in strip's time referential
|
||||||
|
"""
|
||||||
|
|
||||||
|
strips = get_sequences_at_frame(frame, sequences or bpy.context.sequences)
|
||||||
|
|
||||||
|
# exclude muted strips
|
||||||
|
if skip_muted:
|
||||||
|
strips = [strip for strip in strips if not strip.mute]
|
||||||
|
|
||||||
|
if not strips:
|
||||||
|
return None, frame
|
||||||
|
|
||||||
|
# Remove strip not scene type. Switch is only with Scenes
|
||||||
|
for strip in strips:
|
||||||
|
if strip.type != 'SCENE':
|
||||||
|
strips.remove(strip)
|
||||||
|
|
||||||
|
# consider higher strip in stack
|
||||||
|
strip = sorted(strips, key=lambda x: x.channel)[-1]
|
||||||
|
# go deeper when current strip is a MetaSequence
|
||||||
|
if isinstance(strip, MetaSequence):
|
||||||
|
return get_sequence_at_frame(frame, strip.sequences, skip_muted)
|
||||||
|
if isinstance(strip, SceneSequence):
|
||||||
|
# apply time offset to get in sequence's referential
|
||||||
|
frame = frame - strip.frame_start + strip.scene.frame_start
|
||||||
|
# enter scene's sequencer if used as input
|
||||||
|
if strip.scene_input == 'SEQUENCER':
|
||||||
|
return get_sequence_at_frame(frame, strip.scene.sequence_editor.sequences)
|
||||||
|
return strip, frame
|
||||||
|
|
||||||
|
|
||||||
|
def set_scene_frame(scene, frame, force_update_main=False):
|
||||||
|
"""
|
||||||
|
Set `scene` frame_current to `frame` if different.
|
||||||
|
|
||||||
|
:param scene: the scene to update
|
||||||
|
:param frame: the frame value
|
||||||
|
:param force_update_main: whether to force the update of main scene
|
||||||
|
"""
|
||||||
|
options = bpy.context.window_manager.storypencil_settings
|
||||||
|
if scene.frame_current != frame:
|
||||||
|
scene.frame_current = frame
|
||||||
|
scene.frame_set(int(frame))
|
||||||
|
if force_update_main:
|
||||||
|
update_sync(
|
||||||
|
bpy.context, bpy.context.window_manager.storypencil_settings.main_window_id)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_window_from_scene_strip(window: Window, strip: SceneSequence):
|
||||||
|
"""Change the Scene and camera of `window` based on `strip`.
|
||||||
|
|
||||||
|
:param window: [description]
|
||||||
|
:param scene_strip: [description]
|
||||||
|
"""
|
||||||
|
if window.scene != strip.scene:
|
||||||
|
window.scene = strip.scene
|
||||||
|
if strip.scene_camera and strip.scene_camera != window.scene.camera:
|
||||||
|
strip.scene.camera = strip.scene_camera
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def on_frame_changed(*args):
|
||||||
|
"""
|
||||||
|
React to current frame changes and synchronize slave windows.
|
||||||
|
"""
|
||||||
|
# ensure context is fully initialized, i.e not '_RestrictData
|
||||||
|
if not isinstance(bpy.context, Context):
|
||||||
|
return
|
||||||
|
|
||||||
|
# happens in some cases (not sure why)
|
||||||
|
if not bpy.context.window:
|
||||||
|
return
|
||||||
|
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
|
||||||
|
# early return if synchro is disabled / not available
|
||||||
|
if not validate_sync(wm) or len(bpy.data.scenes) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get current window id
|
||||||
|
update_sync(bpy.context)
|
||||||
|
|
||||||
|
|
||||||
|
def update_sync(context: Context, win_id=None):
|
||||||
|
""" Update synchronized Windows based on the current `context`.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param win_id: specify a window id (context.window is used otherwise)
|
||||||
|
"""
|
||||||
|
wm = context.window_manager
|
||||||
|
|
||||||
|
if not win_id:
|
||||||
|
win_id = window_id(context.window)
|
||||||
|
|
||||||
|
main_scene = get_window_from_id(
|
||||||
|
wm, wm.storypencil_settings.main_window_id).scene
|
||||||
|
if not main_scene.sequence_editor:
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if scene's sequence editor has no sequences
|
||||||
|
sequences = main_scene.sequence_editor.sequences
|
||||||
|
if not sequences:
|
||||||
|
return
|
||||||
|
|
||||||
|
# bidirectionnal sync: change main time from slave window
|
||||||
|
if (
|
||||||
|
wm.storypencil_settings.bidirectional
|
||||||
|
and win_id != wm.storypencil_settings.main_window_id
|
||||||
|
and is_slave_window(wm, win_id)
|
||||||
|
):
|
||||||
|
# get strip under time cursor in main window
|
||||||
|
strip, old_frame = get_sequence_at_frame(
|
||||||
|
main_scene.frame_current,
|
||||||
|
sequences=sequences
|
||||||
|
)
|
||||||
|
# only do bidirectional sync if slave window matches the strip at current time in main
|
||||||
|
if not isinstance(strip, SceneSequence) or strip.scene != context.scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
# calculate offset
|
||||||
|
frame_offset = context.scene.frame_current - old_frame
|
||||||
|
if frame_offset == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_main_frame = main_scene.frame_current + frame_offset
|
||||||
|
update_main_time = True
|
||||||
|
# check if a valid scene strip is available under new frame before changing main time
|
||||||
|
f_start = strip.frame_start + strip.frame_offset_start
|
||||||
|
f_end = f_start + strip.frame_final_duration
|
||||||
|
if new_main_frame < f_start or new_main_frame >= f_end:
|
||||||
|
new_strip, _ = get_sequence_at_frame(
|
||||||
|
new_main_frame,
|
||||||
|
main_scene.sequence_editor.sequences,
|
||||||
|
)
|
||||||
|
update_main_time = isinstance(new_strip, SceneSequence)
|
||||||
|
if update_main_time:
|
||||||
|
# update main time change in the next event loop + force the sync system update
|
||||||
|
# because Blender won't trigger a frame_changed event to avoid infinite recursion
|
||||||
|
bpy.app.timers.register(
|
||||||
|
functools.partial(set_scene_frame, main_scene,
|
||||||
|
new_main_frame, True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if current window is not main window
|
||||||
|
if win_id != wm.storypencil_settings.main_window_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
slave_windows = [
|
||||||
|
get_window_from_id(wm, win_id)
|
||||||
|
for win_id
|
||||||
|
in get_slave_window_indices(wm)
|
||||||
|
if win_id and win_id != wm.storypencil_settings.main_window_id
|
||||||
|
]
|
||||||
|
|
||||||
|
# only work with at least 2 windows
|
||||||
|
if not slave_windows:
|
||||||
|
return
|
||||||
|
|
||||||
|
seq, frame = get_sequence_at_frame(main_scene.frame_current, sequences)
|
||||||
|
|
||||||
|
# return if no sequence at current time or not a scene strip
|
||||||
|
if not isinstance(seq, SceneSequence) or not seq.scene:
|
||||||
|
wm.storypencil_settings.main_strip_name = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
wm.storypencil_settings.main_strip_name = seq.name
|
||||||
|
# change the scene on slave windows
|
||||||
|
# warning: only one window's scene can be changed in this event loop,
|
||||||
|
# otherwise it may crashes Blender randomly
|
||||||
|
for idx, win in enumerate(slave_windows):
|
||||||
|
if not win:
|
||||||
|
continue
|
||||||
|
# change first slave window immediately
|
||||||
|
if idx == 0:
|
||||||
|
setup_window_from_scene_strip(win, seq)
|
||||||
|
else:
|
||||||
|
# trigger change in next event loop for other windows
|
||||||
|
bpy.app.timers.register(
|
||||||
|
functools.partial(setup_window_from_scene_strip, win, seq)
|
||||||
|
)
|
||||||
|
|
||||||
|
set_scene_frame(seq.scene, frame)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_all_windows(wm: WindowManager):
|
||||||
|
"""Enable synchronization on all main windows held by `wm`."""
|
||||||
|
wm.storypencil_settings.slave_windows_ids = join_win_ids([
|
||||||
|
window_id(w)
|
||||||
|
for w
|
||||||
|
in get_main_windows_list(wm)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def sync_autoconfig(*args):
|
||||||
|
"""Autoconfigure synchronization system.
|
||||||
|
If a window contains a VSE area on a scene with a valid sequence_editor,
|
||||||
|
makes it main window and enable synchronization on all other main windows.
|
||||||
|
"""
|
||||||
|
main_windows = get_main_windows_list(bpy.context.window_manager)
|
||||||
|
# don't try to go any further if only one main window
|
||||||
|
if len(main_windows) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# look for a main window with a valid sequence editor
|
||||||
|
main = next(
|
||||||
|
(
|
||||||
|
win
|
||||||
|
for win in main_windows
|
||||||
|
if win.scene.sequence_editor
|
||||||
|
and any(area.type == 'SEQUENCE_EDITOR' for area in win.screen.areas)
|
||||||
|
),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
# if any, set as main and activate sync on all other windows
|
||||||
|
if main:
|
||||||
|
bpy.context.window_manager.storypencil_settings.main_window_id = window_id(
|
||||||
|
main)
|
||||||
|
sync_all_windows(bpy.context.window_manager)
|
||||||
|
bpy.context.window_manager.storypencil_settings.active = True
|
||||||
|
|
||||||
|
|
||||||
|
def sync_active_update(self, context):
|
||||||
|
""" Update function for WindowManager.storypencil_settings.active. """
|
||||||
|
# ensure main window is valid, using current context's window if none is set
|
||||||
|
if (
|
||||||
|
self.active
|
||||||
|
and (
|
||||||
|
not self.main_window_id
|
||||||
|
or not get_window_from_id(context.window_manager, self.main_window_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.main_window_id = window_id(context.window)
|
||||||
|
# automatically sync all other windows if nothing was previously set
|
||||||
|
if not self.slave_windows_ids:
|
||||||
|
sync_all_windows(context.window_manager)
|
||||||
|
|
||||||
|
on_frame_changed()
|
||||||
|
|
||||||
|
|
||||||
|
def draw_sync_header(self, context):
|
||||||
|
"""Draw Window sync tools header."""
|
||||||
|
|
||||||
|
wm = context.window_manager
|
||||||
|
self.layout.separator()
|
||||||
|
if wm.get('storypencil_use_new_window') is not None:
|
||||||
|
new_window = wm['storypencil_use_new_window']
|
||||||
|
else:
|
||||||
|
new_window = False
|
||||||
|
|
||||||
|
if not new_window:
|
||||||
|
if context.scene.storypencil_main_workspace:
|
||||||
|
if context.scene.storypencil_main_workspace.name != context.workspace.name:
|
||||||
|
if context.area.ui_type == 'DOPESHEET':
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.operator(STORYPENCIL_OT_Switch.bl_idname,
|
||||||
|
text="Back To VSE")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_sync_sequencer_header(self, context):
|
||||||
|
"""Draw Window sync tools header."""
|
||||||
|
if context.space_data.view_type != 'SEQUENCER':
|
||||||
|
return
|
||||||
|
|
||||||
|
wm = context.window_manager
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.label(text="Scenes:")
|
||||||
|
if context.scene.storypencil_use_new_window:
|
||||||
|
row.operator(STORYPENCIL_OT_SetSyncMainOperator.bl_idname, text="Edit")
|
||||||
|
else:
|
||||||
|
row.operator(STORYPENCIL_OT_Switch.bl_idname, text="Edit")
|
||||||
|
|
||||||
|
row.menu("STORYPENCIL_MT_extra_options", icon='DOWNARROW_HLT', text="")
|
||||||
|
|
||||||
|
row.separator()
|
||||||
|
layout.operator_context = 'INVOKE_REGION_WIN'
|
||||||
|
row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New")
|
||||||
|
|
||||||
|
layout.operator_context = 'INVOKE_DEFAULT'
|
||||||
|
row.separator(factor=0.5)
|
||||||
|
row.operator(STORYPENCIL_OT_RenderAction.bl_idname, text="Render")
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_PG_Settings(PropertyGroup):
|
||||||
|
"""
|
||||||
|
PropertyGroup with storypencil settings.
|
||||||
|
"""
|
||||||
|
active: BoolProperty(
|
||||||
|
name="Synchronize",
|
||||||
|
description=(
|
||||||
|
"Automatically open current Sequence's Scene in other "
|
||||||
|
"Main Windows and activate Time Synchronization"),
|
||||||
|
default=False,
|
||||||
|
update=sync_active_update
|
||||||
|
)
|
||||||
|
|
||||||
|
bidirectional: BoolProperty(
|
||||||
|
name="Bi-directional",
|
||||||
|
description="Enable bi-directional sync to drive Main time from synced Slave Windows",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
main_window_id: StringProperty(
|
||||||
|
name="Main Window ID",
|
||||||
|
description="ID of the window driving the Synchronization",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
slave_windows_ids: StringProperty(
|
||||||
|
name="Slave Windows",
|
||||||
|
description="Serialized Slave Window Indices",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
active_window_index: IntProperty(
|
||||||
|
name="Active Window Index",
|
||||||
|
description="Index for using Window Manager's windows in a UIList",
|
||||||
|
default=0
|
||||||
|
)
|
||||||
|
|
||||||
|
main_strip_name: StringProperty(
|
||||||
|
name="Main Strip Name",
|
||||||
|
description="Scene Strip at current time in the Main window",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
show_main_strip_range: BoolProperty(
|
||||||
|
name="Show Main Strip Range in Slave Windows",
|
||||||
|
description="Draw main Strip's in/out markers in synchronized slave Windows",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Switch manually between Main and Edit Scene and Layout
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
class STORYPENCIL_OT_Switch(Operator):
|
||||||
|
bl_idname = "storypencil.switch"
|
||||||
|
bl_label = "Switch"
|
||||||
|
bl_description = "Switch workspace"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
# Get active strip
|
||||||
|
def act_strip(self, context):
|
||||||
|
scene = context.scene
|
||||||
|
sequences = scene.sequence_editor.sequences
|
||||||
|
if not sequences:
|
||||||
|
return None
|
||||||
|
# Get strip under time cursor
|
||||||
|
strip, old_frame = get_sequence_at_frame(
|
||||||
|
scene.frame_current, sequences=sequences)
|
||||||
|
return strip
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Poll
|
||||||
|
# ------------------------------
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
scene = context.scene
|
||||||
|
if scene.storypencil_main_workspace is None or scene.storypencil_main_scene is None:
|
||||||
|
return False
|
||||||
|
if scene.storypencil_edit_workspace is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Execute button action
|
||||||
|
# ------------------------------
|
||||||
|
def execute(self, context):
|
||||||
|
wm = context.window_manager
|
||||||
|
scene = context.scene
|
||||||
|
wm['storypencil_use_new_window'] = scene.storypencil_use_new_window
|
||||||
|
|
||||||
|
# Switch to Main
|
||||||
|
if scene.storypencil_main_workspace.name != context.workspace.name:
|
||||||
|
cfra_prv = scene.frame_current
|
||||||
|
prv_pin = None
|
||||||
|
if scene.storypencil_main_workspace is not None:
|
||||||
|
if scene.storypencil_main_workspace.use_pin_scene:
|
||||||
|
scene.storypencil_main_workspace.use_pin_scene = False
|
||||||
|
|
||||||
|
context.window.workspace = scene.storypencil_main_workspace
|
||||||
|
|
||||||
|
if scene.storypencil_main_scene is not None:
|
||||||
|
context.window.scene = scene.storypencil_main_scene
|
||||||
|
strip = self.act_strip(context)
|
||||||
|
if strip:
|
||||||
|
context.window.scene.frame_current = int(cfra_prv + strip.frame_start) - 1
|
||||||
|
|
||||||
|
bpy.ops.sequencer.reload()
|
||||||
|
else:
|
||||||
|
# Switch to Edit
|
||||||
|
strip = self.act_strip(context)
|
||||||
|
# save camera
|
||||||
|
if strip is not None and strip.type == "SCENE":
|
||||||
|
# Save data
|
||||||
|
strip.scene.storypencil_main_workspace = scene.storypencil_main_workspace
|
||||||
|
strip.scene.storypencil_main_scene = scene.storypencil_main_scene
|
||||||
|
strip.scene.storypencil_edit_workspace = scene.storypencil_edit_workspace
|
||||||
|
|
||||||
|
# Set workspace and Scene
|
||||||
|
cfra_prv = scene.frame_current
|
||||||
|
if scene.storypencil_edit_workspace.use_pin_scene:
|
||||||
|
scene.storypencil_edit_workspace.use_pin_scene = False
|
||||||
|
|
||||||
|
context.window.workspace = scene.storypencil_edit_workspace
|
||||||
|
context.window.workspace.update_tag()
|
||||||
|
|
||||||
|
context.window.scene = strip.scene
|
||||||
|
active_frame = cfra_prv - strip.frame_start + 1
|
||||||
|
if active_frame < strip.scene.frame_start:
|
||||||
|
active_frame = strip.scene.frame_start
|
||||||
|
context.window.scene.frame_current = int(active_frame)
|
||||||
|
|
||||||
|
# Set camera
|
||||||
|
if strip.scene_input == 'CAMERA':
|
||||||
|
for screen in bpy.data.screens:
|
||||||
|
for area in screen.areas:
|
||||||
|
if area.type == 'VIEW_3D':
|
||||||
|
# select camera as view
|
||||||
|
if strip and strip.scene.camera is not None:
|
||||||
|
area.spaces.active.region_3d.view_perspective = 'CAMERA'
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
211
storypencil/ui.py
Normal file
211
storypencil/ui.py
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from bpy.types import (
|
||||||
|
Menu,
|
||||||
|
Panel,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .synchro import get_main_window, validate_sync, window_id
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_MT_extra_options(Menu):
|
||||||
|
bl_label = "Scene Settings"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
scene = context.scene
|
||||||
|
layout.prop(scene, "storypencil_use_new_window")
|
||||||
|
|
||||||
|
# If no main window nothing else to do
|
||||||
|
if not get_main_window(wm):
|
||||||
|
return
|
||||||
|
|
||||||
|
win_id = window_id(context.window)
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
if not validate_sync(window_manager=wm) or win_id == wm.storypencil_settings.main_window_id:
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(wm.storypencil_settings, "active",
|
||||||
|
text="Timeline Synchronization")
|
||||||
|
row.active = scene.storypencil_use_new_window
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(wm.storypencil_settings,
|
||||||
|
"show_main_strip_range", text="Show Strip Range")
|
||||||
|
row.active = scene.storypencil_use_new_window
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Defines UI panel
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Define panel class for manual switch parameters.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
class STORYPENCIL_PT_Settings(Panel):
|
||||||
|
bl_idname = "STORYPENCIL_PT_Settings"
|
||||||
|
bl_label = "Settings"
|
||||||
|
bl_space_type = 'SEQUENCE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = 'Storypencil'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.space_data.view_type != 'SEQUENCER':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Draw UI
|
||||||
|
# ------------------------------
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_PT_General(Panel):
|
||||||
|
bl_idname = "STORYPENCIL_PT_General"
|
||||||
|
bl_label = "General"
|
||||||
|
bl_space_type = 'SEQUENCE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = 'Storypencil'
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
bl_parent_id = "STORYPENCIL_PT_Settings"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.space_data.view_type != 'SEQUENCER':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Draw UI
|
||||||
|
# ------------------------------
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
scene = context.scene
|
||||||
|
|
||||||
|
setup_ready = scene.storypencil_main_workspace is not None
|
||||||
|
row = layout.row()
|
||||||
|
row.alert = not setup_ready
|
||||||
|
row.prop(scene, "storypencil_main_workspace", text="VSE Workspace")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
if scene.storypencil_main_scene is None:
|
||||||
|
row.alert = True
|
||||||
|
row.prop(scene, "storypencil_main_scene", text="VSE Scene")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
if scene.storypencil_main_workspace and scene.storypencil_edit_workspace:
|
||||||
|
if scene.storypencil_main_workspace.name == scene.storypencil_edit_workspace.name:
|
||||||
|
row.alert = True
|
||||||
|
if scene.storypencil_edit_workspace is None:
|
||||||
|
row.alert = True
|
||||||
|
row.prop(scene, "storypencil_edit_workspace", text="Drawing Workspace")
|
||||||
|
|
||||||
|
|
||||||
|
class STORYPENCIL_PT_RenderPanel(Panel):
|
||||||
|
bl_label = "Render Strips"
|
||||||
|
bl_space_type = 'SEQUENCE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = 'Storypencil'
|
||||||
|
bl_parent_id = "STORYPENCIL_PT_Settings"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.space_data.view_type != 'SEQUENCER':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
scene = context.scene
|
||||||
|
settings = scene.render.image_settings
|
||||||
|
|
||||||
|
is_video = settings.file_format in {'FFMPEG', 'AVI_JPEG', 'AVI_RAW'}
|
||||||
|
row = layout.row()
|
||||||
|
if scene.storypencil_render_render_path is None:
|
||||||
|
row.alert = True
|
||||||
|
row.prop(scene, "storypencil_render_render_path")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_render_onlyselected")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene.render.image_settings, "file_format")
|
||||||
|
|
||||||
|
if settings.file_format == 'FFMPEG':
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene.render.ffmpeg, "format")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.enabled = is_video
|
||||||
|
row.prop(scene.render.ffmpeg, "audio_codec")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_add_render_strip")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.enabled = scene.storypencil_add_render_strip
|
||||||
|
row.prop(scene, "storypencil_render_channel")
|
||||||
|
|
||||||
|
if not is_video:
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_render_step")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_render_numbering")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_add_render_byfolder")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Define panel class for new base scene creation.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
class STORYPENCIL_PT_SettingsNew(Panel):
|
||||||
|
bl_idname = "STORYPENCIL_PT_SettingsNew"
|
||||||
|
bl_label = "New Scenes"
|
||||||
|
bl_space_type = 'SEQUENCE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = 'Storypencil'
|
||||||
|
bl_parent_id = "STORYPENCIL_PT_Settings"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.space_data.view_type != 'SEQUENCER':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Draw UI
|
||||||
|
# ------------------------------
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
scene = context.scene
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_name_prefix", text="Name Prefix")
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_name_suffix", text="Name Suffix")
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(scene, "storypencil_scene_duration", text="Frames")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
if scene.storypencil_base_scene is None:
|
||||||
|
row.alert = True
|
||||||
|
row.prop(scene, "storypencil_base_scene", text="Base Scene")
|
110
storypencil/utils.py
Normal file
110
storypencil/utils.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def redraw_areas_by_type(window, area_type, region_type='WINDOW'):
|
||||||
|
"""Redraw `window`'s areas matching the given `area_type` and optionnal `region_type`."""
|
||||||
|
for area in window.screen.areas:
|
||||||
|
if area.type == area_type:
|
||||||
|
for region in area.regions:
|
||||||
|
if region.type == region_type:
|
||||||
|
region.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
def redraw_all_areas_by_type(context, area_type, region_type='WINDOW'):
|
||||||
|
"""Redraw areas in all windows matching the given `area_type` and optionnal `region_type`."""
|
||||||
|
for window in context.window_manager.windows:
|
||||||
|
redraw_areas_by_type(window, area_type, region_type)
|
||||||
|
|
||||||
|
|
||||||
|
def get_selected_keyframes(context):
|
||||||
|
"""Get list of selected keyframes for any object in the scene. """
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
for ob in context.scene.objects:
|
||||||
|
if ob.type == 'GPENCIL':
|
||||||
|
for gpl in ob.data.layers:
|
||||||
|
for gpf in gpl.frames:
|
||||||
|
if gpf.select:
|
||||||
|
keys.append(gpf.frame_number)
|
||||||
|
|
||||||
|
elif ob.animation_data is not None and ob.animation_data.action is not None:
|
||||||
|
action = ob.animation_data.action
|
||||||
|
for fcu in action.fcurves:
|
||||||
|
for kp in fcu.keyframe_points:
|
||||||
|
if kp.select_control_point:
|
||||||
|
keys.append(int(kp.co[0]))
|
||||||
|
|
||||||
|
keys.sort()
|
||||||
|
unique_keys = list(set(keys))
|
||||||
|
return unique_keys
|
||||||
|
|
||||||
|
|
||||||
|
def find_collections_recursive(root, collections=None):
|
||||||
|
# Initialize the result once
|
||||||
|
if collections is None:
|
||||||
|
collections = []
|
||||||
|
|
||||||
|
def recurse(parent, result):
|
||||||
|
result.append(parent)
|
||||||
|
# Look over children at next level
|
||||||
|
for child in parent.children:
|
||||||
|
recurse(child, result)
|
||||||
|
|
||||||
|
recurse(root, collections)
|
||||||
|
|
||||||
|
return collections
|
||||||
|
|
||||||
|
|
||||||
|
def get_keyframe_list(scene, frame_start, frame_end):
|
||||||
|
"""Get list of frames for any gpencil object in the scene and meshes. """
|
||||||
|
keys = []
|
||||||
|
root = scene.view_layers[0].layer_collection
|
||||||
|
collections = find_collections_recursive(root)
|
||||||
|
|
||||||
|
for laycol in collections:
|
||||||
|
if laycol.exclude is True or laycol.collection.hide_render is True:
|
||||||
|
continue
|
||||||
|
for ob in laycol.collection.objects:
|
||||||
|
if ob.hide_render:
|
||||||
|
continue
|
||||||
|
if ob.type == 'GPENCIL':
|
||||||
|
for gpl in ob.data.layers:
|
||||||
|
if gpl.hide:
|
||||||
|
continue
|
||||||
|
for gpf in gpl.frames:
|
||||||
|
if frame_start <= gpf.frame_number <= frame_end:
|
||||||
|
keys.append(gpf.frame_number)
|
||||||
|
|
||||||
|
# Animation at object level
|
||||||
|
if ob.animation_data is not None and ob.animation_data.action is not None:
|
||||||
|
action = ob.animation_data.action
|
||||||
|
for fcu in action.fcurves:
|
||||||
|
for kp in fcu.keyframe_points:
|
||||||
|
if frame_start <= int(kp.co[0]) <= frame_end:
|
||||||
|
keys.append(int(kp.co[0]))
|
||||||
|
|
||||||
|
# Animation at datablock level
|
||||||
|
if ob.type != 'GPENCIL':
|
||||||
|
data = ob.data
|
||||||
|
if data and data.animation_data is not None and data.animation_data.action is not None:
|
||||||
|
action = data.animation_data.action
|
||||||
|
for fcu in action.fcurves:
|
||||||
|
for kp in fcu.keyframe_points:
|
||||||
|
if frame_start <= int(kp.co[0]) <= frame_end:
|
||||||
|
keys.append(int(kp.co[0]))
|
||||||
|
|
||||||
|
# Scene Markers
|
||||||
|
for m in scene.timeline_markers:
|
||||||
|
if frame_start <= m.frame <= frame_end and m.camera is not None:
|
||||||
|
keys.append(int(m.frame))
|
||||||
|
|
||||||
|
# If no animation or markers, must add first frame
|
||||||
|
if len(keys) == 0:
|
||||||
|
keys.append(int(frame_start))
|
||||||
|
|
||||||
|
unique_keys = list(set(keys))
|
||||||
|
unique_keys.sort()
|
||||||
|
return unique_keys
|
Reference in New Issue
Block a user