Storypencil: New Storyboard add-on base on grease pencil

This add-on was in addons-contrib and now has been moved
to official.
This commit is contained in:
Antonio Vazquez 2022-12-07 19:43:12 +01:00
parent 90c87dd771
commit a61732a0aa
Notes: blender-bot 2023-02-13 13:52:55 +01:00
Referenced by issue blender/blender#102967: 3.4: Potential candidates for corrective releases
Referenced by issue blender/blender#102967, 3.4: Potential candidates for corrective releases
7 changed files with 1987 additions and 0 deletions

239
storypencil/__init__.py Normal file
View File

@ -0,0 +1,239 @@
# 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": (1, 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_SyncToggleSecondary,
synchro.STORYPENCIL_OT_SetSyncMainOperator,
synchro.STORYPENCIL_OT_AddSecondaryWindowOperator,
synchro.STORYPENCIL_OT_Switch,
synchro.STORYPENCIL_OT_TabSwitch,
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)
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name="Sequencer", space_type="SEQUENCE_EDITOR")
kmi = km.keymap_items.new(
idname="storypencil.tabswitch",
type="TAB",
value="PRESS",
shift=False, ctrl=False, alt = False, oskey=False,
)
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
register_keymaps()
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="Template 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.secondary_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():
unregister_keymaps()
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()

View File

@ -0,0 +1,179 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import typing
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
from .utils import (redraw_all_areas_by_type)
from .synchro import (is_secondary_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
gpu.state.blend_set('ALPHA')
self.shader.uniform_float("color", color)
batch = batch_for_shader(self.shader, 'TRIS', {"pos": coords}, indices=indices)
batch.program_set(self.shader)
batch.draw()
gpu.state.blend_set('NONE')
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_secondary_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
View 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
View 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 Template 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"}

794
storypencil/synchro.py Normal file
View File

@ -0,0 +1,794 @@
# 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, secondary_window):
if main_window is None or secondary_window is None:
return
secondary_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace
secondary_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene
secondary_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)
secondary_window = get_secondary_window(wm)
# Active sync
options.active = True
if secondary_window is None:
# Open a new window
if len(main_windows) < 2:
bpy.ops.storypencil.create_secondary_window()
secondary_window = get_secondary_window(wm)
self.copy_settings(get_main_window(wm), secondary_window)
return {'FINISHED'}
else:
# Reuse the existing window
secondary_window = get_not_main_window(wm)
else:
# Open new secondary
if len(main_windows) < 2:
bpy.ops.storypencil.create_secondary_window()
secondary_window = get_secondary_window(wm)
self.copy_settings(get_main_window(wm), secondary_window)
return {'FINISHED'}
else:
# Reuse the existing window
secondary_window = get_not_main_window(wm)
if secondary_window:
enable_secondary_window(wm, window_id(secondary_window))
win_id = window_id(secondary_window)
self.copy_settings(get_main_window(wm), secondary_window)
bpy.ops.storypencil.sync_window_bring_front(win_id=win_id)
return {'FINISHED'}
class STORYPENCIL_OT_AddSecondaryWindowOperator(Operator):
bl_idname = "storypencil.create_secondary_window"
bl_label = "Create Secondary Window"
bl_description = "Create a Secondary 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_secondary_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_secondary_window(context, new_window)
return {'FINISHED'}
def configure_new_secondary_window(self, context, new_window):
wrk_name = context.scene.storypencil_edit_workspace.name
# 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
with context.temp_override(window=new_window):
bpy.ops.workspace.append_activate(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):
win = get_window_from_id(context.window_manager, self.win_id)
if not win:
return {'CANCELLED'}
with context.temp_override(window=win):
bpy.ops.wm.window_fullscreen_toggle()
bpy.ops.wm.window_fullscreen_toggle()
return {'FINISHED'}
class STORYPENCIL_OT_WindowCloseOperator(Operator):
bl_idname = "storypencil.close_secondary_window"
bl_label = "Close Window"
bl_description = "Close a specific Window"
bl_options = {'INTERNAL'}
win_id: bpy.props.StringProperty()
def execute(self, context):
win = get_window_from_id(context.window_manager, self.win_id)
if not win:
return {'CANCELLED'}
with context.temp_override(window=win):
bpy.ops.wm.window_close()
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_secondary_window_indices(wm: WindowManager) -> List[str]:
"""Get secondary Windows indices as a list of IDs
:param wm: the WindowManager to consider
:return: the list of secondary Windows IDs
"""
return split_win_ids(wm.storypencil_settings.secondary_windows_ids)
def is_secondary_window(window_manager: WindowManager, win_id: str) -> bool:
"""Return wether the Window identified by 'win_id' is a secondary window.
:return: whether this Window is a sync secondary
"""
return win_id in get_secondary_window_indices(window_manager)
def enable_secondary_window(wm: WindowManager, win_id: str):
"""Enable the secondary status of a Window.
:param wm: the WindowManager instance
:param win_id: the id of the window
"""
secondary_indices = get_secondary_window_indices(wm)
win_id_str = win_id
# Delete old indice if exist
if win_id_str in secondary_indices:
secondary_indices.remove(win_id_str)
# Add indice
secondary_indices.append(win_id_str)
# rebuild the whole list of valid secondary windows
secondary_indices = [
idx for idx in secondary_indices if get_window_from_id(wm, idx)]
wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_indices)
def toggle_secondary_window(wm: WindowManager, win_id: str):
"""Toggle the secondary status of a Window.
:param wm: the WindowManager instance
:param win_id: the id of the window
"""
secondary_indices = get_secondary_window_indices(wm)
win_id_str = win_id
if win_id_str in secondary_indices:
secondary_indices.remove(win_id_str)
else:
secondary_indices.append(win_id_str)
# rebuild the whole list of valid secondary windows
secondary_indices = [
idx for idx in secondary_indices if get_window_from_id(wm, idx)]
wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_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_secondary_window(wm: WindowManager) -> Window:
"""Get the first secondary Window
:param wm: the WindowManager instance
:returns: the Window or None
"""
for w in wm.windows:
win_id = window_id(w)
if is_secondary_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_SyncToggleSecondary(Operator):
bl_idname = "storypencil.sync_toggle_secondary"
bl_label = "Toggle Secondary 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_secondary_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 = int(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 secondary 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 secondary window
if (
win_id != wm.storypencil_settings.main_window_id
and is_secondary_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 secondary 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
secondary_windows = [
get_window_from_id(wm, win_id)
for win_id
in get_secondary_window_indices(wm)
if win_id and win_id != wm.storypencil_settings.main_window_id
]
# only work with at least 2 windows
if not secondary_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 secondary 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(secondary_windows):
if not win:
continue
# change first secondary 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.secondary_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.secondary_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
)
main_window_id: StringProperty(
name="Main Window ID",
description="ID of the window driving the Synchronization",
default="",
)
secondary_windows_ids: StringProperty(
name="Secondary Windows",
description="Serialized Secondary 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 Secondary Windows",
description="Draw main Strip's in/out markers in synchronized secondary 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"}
class STORYPENCIL_OT_TabSwitch(Operator):
bl_idname = "storypencil.tabswitch"
bl_label = "Switch using tab key"
bl_description = "Wrapper used to handle the Tab key to switch"
bl_options = {'INTERNAL'}
def execute(self, context):
if context.scene.storypencil_use_new_window:
bpy.ops.storypencil.sync_set_main('INVOKE_DEFAULT', True)
else:
bpy.ops.storypencil.switch('INVOKE_DEFAULT', True)
return {'FINISHED'}

211
storypencil/ui.py Normal file
View 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="Template Scene")

110
storypencil/utils.py Normal file
View 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