Blender Kitsu: Move Render Review into Blender Kitsu #296
@ -1,60 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
import bpy
|
||||
|
||||
from . import (
|
||||
util,
|
||||
props,
|
||||
opsdata,
|
||||
checksqe,
|
||||
ops,
|
||||
ui,
|
||||
draw,
|
||||
)
|
||||
|
||||
|
||||
_need_reload = "ops" in locals()
|
||||
|
||||
|
||||
if _need_reload:
|
||||
import importlib
|
||||
|
||||
util = importlib.reload(util)
|
||||
props = importlib.reload(props)
|
||||
opsdata = importlib.reload(opsdata)
|
||||
checksqe = importlib.reload(checksqe)
|
||||
ops = importlib.reload(ops)
|
||||
ui = importlib.reload(ui)
|
||||
draw = importlib.reload(draw)
|
||||
|
||||
|
||||
def register():
|
||||
props.register()
|
||||
ops.register()
|
||||
ui.register()
|
||||
draw.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
draw.unregister()
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
props.unregister()
|
@ -1,94 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def _do_ranges_collide(range1: range, range2: range) -> bool:
|
||||
"""Whether the two ranges collide with each other ."""
|
||||
# usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140)
|
||||
# first and last frame can be the same for each strip
|
||||
range2 = range(range2.start + 1, range2.stop - 1)
|
||||
|
||||
if not range1:
|
||||
return True # empty range is subset of anything
|
||||
|
||||
if not range2:
|
||||
return False # non-empty range can't be subset of empty range
|
||||
|
||||
if len(range1) > 1 and range1.step % range2.step:
|
||||
return False # must have a single value or integer multiple step
|
||||
|
||||
if range(range1.start + 1, range1.stop - 1) == range2:
|
||||
return True
|
||||
|
||||
if range2.start in range1 or range2[-1] in range1:
|
||||
return True
|
||||
|
||||
return range1.start in range2 or range1[-1] in range2
|
||||
|
||||
|
||||
def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]:
|
||||
"""
|
||||
Scans sequence editor and returns a dictionary. It contains a key for each channel
|
||||
and a list of ranges with the occupied frame ranges as values.
|
||||
"""
|
||||
# {'1': [(101, 213), (300, 320)]}.
|
||||
ranges: Dict[str, List[range]] = {}
|
||||
|
||||
# Populate ranges.
|
||||
for strip in context.scene.sequence_editor.sequences_all:
|
||||
ranges.setdefault(str(strip.channel), [])
|
||||
ranges[str(strip.channel)].append(
|
||||
range(strip.frame_final_start, strip.frame_final_end + 1)
|
||||
)
|
||||
|
||||
# Sort ranges tuple list.
|
||||
for channel in ranges:
|
||||
liste = ranges[channel]
|
||||
ranges[channel] = sorted(liste, key=lambda item: item.start)
|
||||
|
||||
return ranges
|
||||
|
||||
|
||||
def get_occupied_ranges_for_strips(sequences: List[bpy.types.Sequence]) -> List[range]:
|
||||
"""
|
||||
Scans input list of sequences and returns a list of ranges that represent the occupied frame ranges.
|
||||
"""
|
||||
ranges: List[range] = []
|
||||
|
||||
# Populate ranges.
|
||||
for strip in sequences:
|
||||
ranges.append(range(strip.frame_final_start, strip.frame_final_end + 1))
|
||||
|
||||
# Sort ranges tuple list.
|
||||
ranges.sort(key=lambda item: item.start)
|
||||
return ranges
|
||||
|
||||
|
||||
def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool:
|
||||
for r in occupied_ranges:
|
||||
# Range(101, 150).
|
||||
if _do_ranges_collide(range_to_check, r):
|
||||
return True
|
||||
continue
|
||||
return False
|
@ -1,263 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>.
|
||||
|
||||
# This file is copied from the blender-cloud-addon https://developer.blender.org/diffusion/BCA/
|
||||
# Author of this file is: Sybren A. Stuevel
|
||||
# Modified by: Paul Golter
|
||||
|
||||
import typing
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
APPROVED_COLOR = (0.24, 1, 0.139, 0.7)
|
||||
PUSHED_TO_EDIT_COLOR = (0.8, .8, 0.1, 0.5)
|
||||
|
||||
# Glsl.
|
||||
gpu_vertex_shader = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
|
||||
layout (location = 0) in vec2 pos;
|
||||
layout (location = 1) in vec4 color;
|
||||
|
||||
out vec4 lineColor; // output to the fragment shader
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.x, pos.y, 0.0, 1.0);
|
||||
lineColor = color;
|
||||
}
|
||||
"""
|
||||
|
||||
gpu_fragment_shader = """
|
||||
out vec4 fragColor;
|
||||
in vec4 lineColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
fragColor = lineColor;
|
||||
}
|
||||
"""
|
||||
|
||||
Float2 = typing.Tuple[float, float]
|
||||
Float3 = typing.Tuple[float, float, float]
|
||||
Float4 = typing.Tuple[float, float, float, float]
|
||||
LINE_WIDTH = 6
|
||||
|
||||
|
||||
class LineDrawer:
|
||||
def __init__(self):
|
||||
self._format = gpu.types.GPUVertFormat()
|
||||
self._pos_id = self._format.attr_add(
|
||||
id="pos", comp_type="F32", len=2, fetch_mode="FLOAT"
|
||||
)
|
||||
self._color_id = self._format.attr_add(
|
||||
id="color", comp_type="F32", len=4, fetch_mode="FLOAT"
|
||||
)
|
||||
|
||||
self.shader = gpu.types.GPUShader(gpu_vertex_shader, gpu_fragment_shader)
|
||||
|
||||
def draw(self, coords: typing.List[Float2], colors: typing.List[Float4]):
|
||||
global LINE_WIDTH
|
||||
|
||||
if not coords:
|
||||
return
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(LINE_WIDTH)
|
||||
|
||||
vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format)
|
||||
vbo.attr_fill(id=self._pos_id, data=coords)
|
||||
vbo.attr_fill(id=self._color_id, data=colors)
|
||||
|
||||
batch = gpu.types.GPUBatch(type="LINES", buf=vbo)
|
||||
batch.program_set(self.shader)
|
||||
batch.draw()
|
||||
|
||||
|
||||
def get_strip_rectf(strip) -> Float4:
|
||||
# Get x and y in terms of the grid's frames and channels.
|
||||
x1 = strip.frame_final_start
|
||||
x2 = strip.frame_final_end
|
||||
# Seems to be a 5 % offset from channel top start of strip.
|
||||
y1 = strip.channel + 0.05
|
||||
y2 = strip.channel - 0.05 + 1
|
||||
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
def line_in_strip(
|
||||
strip_coords: Float4,
|
||||
pixel_size_x: float,
|
||||
color: Float4,
|
||||
line_height_factor: float,
|
||||
out_coords: typing.List[Float2],
|
||||
out_colors: typing.List[Float4],
|
||||
):
|
||||
# Strip coords.
|
||||
s_x1, s_y1, s_x2, s_y2 = strip_coords
|
||||
|
||||
# Calculate line height with factor.
|
||||
line_y = (1 - line_height_factor) * s_y1 + line_height_factor * s_y2
|
||||
|
||||
# if strip is shorter than line_width use stips s_x2
|
||||
# line_x2 = s_x1 + line_width if (s_x2 - s_x1 > line_width) else s_x2
|
||||
line_x2 = s_x2
|
||||
|
||||
# Be careful not to draw over the current frame line.
|
||||
cf_x = bpy.context.scene.frame_current_final
|
||||
|
||||
# TODO(Sybren): figure out how to pass one colour per line,
|
||||
# instead of one colour per vertex.
|
||||
out_coords.append((s_x1, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
if s_x1 < cf_x < line_x2:
|
||||
# Bad luck, the line passes our strip, so draw two lines.
|
||||
out_coords.append((cf_x - pixel_size_x, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
out_coords.append((cf_x + pixel_size_x, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
out_coords.append((line_x2, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
|
||||
def draw_callback_px(line_drawer: LineDrawer):
|
||||
global LINE_WIDTH
|
||||
|
||||
context = bpy.context
|
||||
|
||||
if not context.scene.sequence_editor:
|
||||
return
|
||||
|
||||
# From . import shown_strips.
|
||||
|
||||
region = context.region
|
||||
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
|
||||
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
|
||||
one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1)
|
||||
pixel_size_x = one_pixel_further_x - xwin1
|
||||
|
||||
# Strips = shown_strips(context).
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
coords = [] # type: typing.List[Float2]
|
||||
colors = [] # type: typing.List[Float4]
|
||||
|
||||
# Collect all the lines (vertex coords + vertex colours) to draw.
|
||||
for strip in strips:
|
||||
|
||||
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords.
|
||||
strip_coords = get_strip_rectf(strip)
|
||||
|
||||
# Check if any of the coordinates are out of bounds.
|
||||
if (
|
||||
strip_coords[0] > xwin2
|
||||
or strip_coords[2] < xwin1
|
||||
or strip_coords[1] > ywin2
|
||||
or strip_coords[3] < ywin1
|
||||
):
|
||||
continue
|
||||
|
||||
if strip.rr.is_approved:
|
||||
line_in_strip(
|
||||
strip_coords,
|
||||
pixel_size_x,
|
||||
APPROVED_COLOR,
|
||||
0.05,
|
||||
coords,
|
||||
colors,
|
||||
)
|
||||
elif strip.rr.is_pushed_to_edit:
|
||||
line_in_strip(
|
||||
strip_coords,
|
||||
pixel_size_x,
|
||||
PUSHED_TO_EDIT_COLOR,
|
||||
0.05,
|
||||
coords,
|
||||
colors,
|
||||
)
|
||||
|
||||
line_drawer.draw(coords, colors)
|
||||
|
||||
|
||||
def tag_redraw_all_sequencer_editors():
|
||||
context = bpy.context
|
||||
|
||||
# Py cant access notifiers.
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
# This is a list so it can be changed instead of set
|
||||
# if it is only changed, it does not have to be declared as a global everywhere
|
||||
cb_handle = []
|
||||
|
||||
|
||||
def callback_enable():
|
||||
global cb_handle
|
||||
|
||||
if cb_handle:
|
||||
return
|
||||
|
||||
# Doing GPU stuff in the background crashes Blender, so let's not.
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
line_drawer = LineDrawer()
|
||||
cb_handle[:] = (
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_add(
|
||||
draw_callback_px, (line_drawer,), "WINDOW", "POST_VIEW"
|
||||
),
|
||||
)
|
||||
|
||||
tag_redraw_all_sequencer_editors()
|
||||
|
||||
|
||||
def callback_disable():
|
||||
global cb_handle
|
||||
|
||||
if not cb_handle:
|
||||
return
|
||||
|
||||
try:
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], "WINDOW")
|
||||
except ValueError:
|
||||
# Thrown when already removed.
|
||||
pass
|
||||
cb_handle.clear()
|
||||
|
||||
tag_redraw_all_sequencer_editors()
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def register():
|
||||
callback_enable()
|
||||
|
||||
|
||||
def unregister():
|
||||
callback_disable()
|
@ -1,25 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
|
||||
class NoImageSequenceAvailableException(Exception):
|
||||
"""
|
||||
Error raised when trying to gather image sequence in folder but no files are existent
|
||||
"""
|
File diff suppressed because it is too large
Load Diff
@ -1,431 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Set, Union, Optional, List, Dict, Any, Tuple
|
||||
|
||||
import bpy
|
||||
|
||||
from . import vars, checksqe, util
|
||||
from .. import prefs, cache
|
||||
from ..sqe import opsdata as sqe_opsdata
|
||||
from .exception import NoImageSequenceAvailableException
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
copytree_list: List[Path] = []
|
||||
copytree_num_of_items: int = 0
|
||||
|
||||
|
||||
def copytree_verbose(src: Union[str, Path], dest: Union[str, Path], **kwargs):
|
||||
_copytree_init_progress_update(Path(src))
|
||||
shutil.copytree(src, dest, copy_function=_copy2_tree_progress, **kwargs)
|
||||
_copytree_clear_progress_update()
|
||||
|
||||
|
||||
def _copytree_init_progress_update(source_dir: Path):
|
||||
global copytree_num_of_items
|
||||
file_list = [f for f in source_dir.glob("**/*") if f.is_file()]
|
||||
copytree_num_of_items = len(file_list)
|
||||
|
||||
|
||||
def _copy2_tree_progress(src, dst):
|
||||
"""
|
||||
Function that can be used for copy_function
|
||||
argument on shutil.copytree function.
|
||||
Logs every item that is currently copied.
|
||||
"""
|
||||
global copytree_num_of_items
|
||||
global copytree_list
|
||||
|
||||
copytree_list.append(Path(src))
|
||||
progress = round((len(copytree_list) * 100) / copytree_num_of_items)
|
||||
logger.info("Copying %s (%i%%)", src, progress)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def _copytree_clear_progress_update():
|
||||
global copytree_num_of_items
|
||||
|
||||
copytree_num_of_items = 0
|
||||
copytree_list.clear()
|
||||
|
||||
|
||||
def get_valid_cs_sequences(
|
||||
context: bpy.types.Context, sequence_list: List[bpy.types.Sequence] = []
|
||||
) -> List[bpy.types.Sequence]:
|
||||
|
||||
sequences: List[bpy.types.Sequence] = []
|
||||
|
||||
if sequence_list:
|
||||
sequences = sequence_list
|
||||
else:
|
||||
sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all
|
||||
|
||||
if cache.project_active_get():
|
||||
|
||||
valid_sequences = [
|
||||
s
|
||||
for s in sequences
|
||||
if s.type in ["MOVIE", "IMAGE"] and not s.mute and not s.kitsu.initialized
|
||||
]
|
||||
else:
|
||||
valid_sequences = [s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute]
|
||||
|
||||
return valid_sequences
|
||||
|
||||
|
||||
def get_frames_root_dir(strip: bpy.types.Sequence) -> Path:
|
||||
# sf = shot_frames | fo = farm_output.
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
fo_dir = get_strip_folder(strip)
|
||||
sf_dir = addon_prefs.frames_root_dir / fo_dir.parent.relative_to(fo_dir.parents[3])
|
||||
|
||||
return sf_dir
|
||||
|
||||
|
||||
def get_strip_folder(strip: bpy.types.Sequence) -> Path:
|
||||
if hasattr(strip, 'directory'):
|
||||
return Path(strip.directory)
|
||||
else:
|
||||
return Path(strip.filepath).parent
|
||||
|
||||
|
||||
def get_shot_previews_path(strip: bpy.types.Sequence) -> Path:
|
||||
# Fo > farm_output.
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
fo_dir = get_strip_folder(strip)
|
||||
shot_previews_dir = addon_prefs.shot_playblast_root_dir / fo_dir.parent.relative_to(
|
||||
fo_dir.parents[3]
|
||||
)
|
||||
|
||||
return shot_previews_dir
|
||||
|
||||
|
||||
def get_shot_dot_task_type(path: Path):
|
||||
return path.parent.name
|
||||
|
||||
|
||||
def get_farm_output_mp4_path(strip: bpy.types.Sequence) -> Path:
|
||||
render_dir = get_strip_folder(strip)
|
||||
return get_farm_output_mp4_path_from_folder(render_dir)
|
||||
|
||||
|
||||
def get_farm_output_mp4_path_from_folder(render_dir: str) -> Path:
|
||||
render_dir = Path(render_dir)
|
||||
shot_name = render_dir.parent.name
|
||||
|
||||
# 070_0040_A.lighting-101-136.mp4 #farm always does .lighting not .comp
|
||||
# because flamenco writes in and out frame in filename we need check the first and
|
||||
# last frame in the folder
|
||||
preview_seq = get_best_preview_sequence(render_dir)
|
||||
|
||||
mp4_filename = f"{shot_name}-{int(preview_seq[0].stem)}-{int(preview_seq[-1].stem)}.mp4"
|
||||
|
||||
return render_dir / mp4_filename
|
||||
|
||||
|
||||
def get_best_preview_sequence(dir: Path) -> List[Path]:
|
||||
|
||||
files: List[List[Path]] = gather_files_by_suffix(
|
||||
dir, output=dict, search_suffixes=[".jpg", ".png"]
|
||||
)
|
||||
if not files:
|
||||
raise NoImageSequenceAvailableException(f"No preview files found in: {dir.as_posix()}")
|
||||
|
||||
# Select the right images sequence.
|
||||
if len(files) == 1:
|
||||
# If only one image sequence available take that.
|
||||
preview_seq = files[list(files.keys())[0]]
|
||||
|
||||
# Both jpg and png available.
|
||||
else:
|
||||
# If same amount of frames take png.
|
||||
if len(files[".jpg"]) == len(files[".png"]):
|
||||
preview_seq = files[".png"]
|
||||
else:
|
||||
# If not, take whichever is longest.
|
||||
preview_seq = [files[".jpg"], files[".png"]].sort(key=lambda x: len(x))[-1]
|
||||
|
||||
return preview_seq
|
||||
|
||||
|
||||
def get_shot_frames_backup_path(strip: bpy.types.Sequence) -> Path:
|
||||
fs_dir = get_frames_root_dir(strip)
|
||||
return fs_dir.parent / f"_backup.{fs_dir.name}"
|
||||
|
||||
|
||||
def get_shot_frames_metadata_path(strip: bpy.types.Sequence) -> Path:
|
||||
fs_dir = get_frames_root_dir(strip)
|
||||
return fs_dir.parent / "metadata.json"
|
||||
|
||||
|
||||
def get_shot_previews_metadata_path(strip: bpy.types.Sequence) -> Path:
|
||||
fs_dir = get_shot_previews_path(strip)
|
||||
return fs_dir / "metadata.json"
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
with open(path.as_posix(), "r") as file:
|
||||
obj = json.load(file)
|
||||
return obj
|
||||
|
||||
|
||||
def save_to_json(obj: Any, path: Path) -> None:
|
||||
with open(path.as_posix(), "w") as file:
|
||||
json.dump(obj, file, indent=4)
|
||||
|
||||
|
||||
def update_sequence_statuses(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Sequence]:
|
||||
return update_is_approved(context), update_is_pushed_to_edit(context)
|
||||
|
||||
|
||||
def update_is_approved(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Sequence]:
|
||||
sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render]
|
||||
|
||||
approved_strips = []
|
||||
|
||||
for s in sequences:
|
||||
metadata_path = get_shot_frames_metadata_path(s)
|
||||
if not metadata_path.exists():
|
||||
continue
|
||||
json_obj = load_json(metadata_path) # TODO: prevent opening same json multi times
|
||||
|
||||
if Path(json_obj["source_current"]) == get_strip_folder(s):
|
||||
s.rr.is_approved = True
|
||||
approved_strips.append(s)
|
||||
logger.info("Detected approved strip: %s", s.name)
|
||||
else:
|
||||
s.rr.is_approved = False
|
||||
|
||||
return approved_strips
|
||||
|
||||
|
||||
def update_is_pushed_to_edit(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Sequence]:
|
||||
sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render]
|
||||
|
||||
pushed_strips = []
|
||||
|
||||
for s in sequences:
|
||||
metadata_path = get_shot_previews_metadata_path(s)
|
||||
if not metadata_path.exists():
|
||||
continue
|
||||
|
||||
json_obj = load_json(metadata_path)
|
||||
|
||||
valid_paths = {Path(value).parent for _key, value in json_obj.items()}
|
||||
|
||||
if get_strip_folder(s) in valid_paths:
|
||||
s.rr.is_pushed_to_edit = True
|
||||
pushed_strips.append(s)
|
||||
logger.info("Detected pushed strip: %s", s.name)
|
||||
else:
|
||||
s.rr.is_pushed_to_edit = False
|
||||
|
||||
return pushed_strips
|
||||
|
||||
|
||||
def gather_files_by_suffix(
|
||||
dir: Path, output=str, search_suffixes: List[str] = [".jpg", ".png", ".exr"]
|
||||
) -> Union[str, List, Dict]:
|
||||
"""
|
||||
Gathers files in dir that end with an extension in search_suffixes.
|
||||
Supported values for output: str, list, dict
|
||||
"""
|
||||
|
||||
files: Dict[str, List[Path]] = {}
|
||||
|
||||
# Gather files.
|
||||
for f in dir.iterdir():
|
||||
if not f.is_file():
|
||||
continue
|
||||
|
||||
for suffix in search_suffixes:
|
||||
if f.suffix == suffix:
|
||||
files.setdefault(suffix, [])
|
||||
files[suffix].append(f)
|
||||
|
||||
# Sort.
|
||||
for suffix, file_list in files.items():
|
||||
files[suffix] = sorted(file_list, key=lambda f: f.name)
|
||||
|
||||
# Return.
|
||||
if output == str:
|
||||
return_str = ""
|
||||
for suffix, file_list in files.items():
|
||||
return_str += f" | {suffix}: {len(file_list)}"
|
||||
|
||||
# Replace first occurence, we dont want that at the beginning.
|
||||
return_str = return_str.replace(" | ", "", 1)
|
||||
|
||||
return return_str
|
||||
|
||||
elif output == dict:
|
||||
return files
|
||||
|
||||
elif output == list:
|
||||
output_list = []
|
||||
for suffix, file_list in files.items():
|
||||
output_list.append(file_list)
|
||||
|
||||
return output_list
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Supported output types are: str, dict, list. {str(output)} not implemented yet."
|
||||
)
|
||||
|
||||
|
||||
def gen_frames_found_text(dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"]) -> str:
|
||||
files_dict = gather_files_by_suffix(dir, output=dict, search_suffixes=search_suffixes)
|
||||
|
||||
frames_found_text = "" # frames found text will be used in ui
|
||||
for suffix, file_list in files_dict.items():
|
||||
frames_found_text += f" | {suffix}: {len(file_list)}"
|
||||
|
||||
# Replace first occurence, we dont want that at the beginning.
|
||||
frames_found_text = frames_found_text.replace(
|
||||
" | ",
|
||||
"",
|
||||
1,
|
||||
)
|
||||
return frames_found_text
|
||||
|
||||
|
||||
def is_sequence_dir(dir: Path) -> bool:
|
||||
return dir.parent.name == "shots"
|
||||
|
||||
|
||||
def is_shot_dir(dir: Path) -> bool:
|
||||
return dir.parent.parent.name == "shots"
|
||||
|
||||
|
||||
def get_shot_name_from_dir(dir: Path) -> str:
|
||||
return dir.stem # 060_0010_A.lighting > 060_0010_A
|
||||
|
||||
|
||||
def get_image_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]:
|
||||
image_editor = None
|
||||
|
||||
for area in context.screen.areas:
|
||||
if area.type == "IMAGE_EDITOR":
|
||||
image_editor = area
|
||||
|
||||
return image_editor
|
||||
|
||||
|
||||
def get_sqe_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]:
|
||||
sqe_editor = None
|
||||
|
||||
for area in context.screen.areas:
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
sqe_editor = area
|
||||
|
||||
return sqe_editor
|
||||
|
||||
|
||||
def fit_frame_range_to_strips(
|
||||
context: bpy.types.Context, strips: Optional[List[bpy.types.Sequence]] = None
|
||||
) -> Tuple[int, int]:
|
||||
def get_sort_tuple(strip: bpy.types.Sequence) -> Tuple[int, int]:
|
||||
return (strip.frame_final_start, strip.frame_final_duration)
|
||||
|
||||
if not strips:
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
if not strips:
|
||||
return (0, 0)
|
||||
|
||||
strips = list(strips)
|
||||
strips.sort(key=get_sort_tuple)
|
||||
|
||||
context.scene.frame_start = strips[0].frame_final_start
|
||||
context.scene.frame_end = strips[-1].frame_final_end - 1
|
||||
|
||||
return (context.scene.frame_start, context.scene.frame_end)
|
||||
|
||||
|
||||
def get_top_level_valid_strips_continious(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Sequence]:
|
||||
|
||||
sequences_tmp = get_valid_cs_sequences(
|
||||
context, sequence_list=list(context.scene.sequence_editor.sequences_all)
|
||||
)
|
||||
|
||||
sequences_tmp.sort(key=lambda s: (s.channel, s.frame_final_start), reverse=True)
|
||||
sequences: List[bpy.types.Sequence] = []
|
||||
|
||||
for strip in sequences_tmp:
|
||||
|
||||
occ_ranges = checksqe.get_occupied_ranges_for_strips(sequences)
|
||||
s_range = range(strip.frame_final_start, strip.frame_final_end + 1)
|
||||
|
||||
if not checksqe.is_range_occupied(s_range, occ_ranges):
|
||||
sequences.append(strip)
|
||||
|
||||
return sequences
|
||||
|
||||
|
||||
def setup_color_management(context: bpy.types.Context) -> None:
|
||||
if context.scene.view_settings.view_transform != 'Standard':
|
||||
context.scene.view_settings.view_transform = 'Standard'
|
||||
logger.info("Set view transform to: Standard")
|
||||
|
||||
|
||||
def is_active_project() -> bool:
|
||||
return bool(cache.project_active_get())
|
||||
|
||||
|
||||
def link_strip_by_name(
|
||||
context: bpy.types.Context,
|
||||
strip: bpy.types.Sequence,
|
||||
shot_name: str,
|
||||
sequence_name: str,
|
||||
) -> None:
|
||||
# Get seq and shot.
|
||||
active_project = cache.project_active_get()
|
||||
seq = active_project.get_sequence_by_name(sequence_name)
|
||||
shot = active_project.get_shot_by_name(seq, shot_name)
|
||||
|
||||
if not shot:
|
||||
logger.error("Unable to find shot %s on kitsu", shot_name)
|
||||
return
|
||||
|
||||
sqe_opsdata.link_metadata_strip(context, shot, seq, strip)
|
||||
|
||||
# Log.
|
||||
t = "Linked strip: %s to shot: %s with ID: %s" % (
|
||||
strip.name,
|
||||
shot.name,
|
||||
shot.id,
|
||||
)
|
||||
logger.info(t)
|
||||
util.redraw_ui()
|
@ -1,102 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
from typing import Set, Union, Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class RR_isolate_collection_prop(bpy.types.PropertyGroup):
|
||||
mute: bpy.props.BoolProperty()
|
||||
|
||||
|
||||
class RR_property_group_scene(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
render_dir: bpy.props.StringProperty(name="Render Directory", subtype="DIR_PATH")
|
||||
isolate_view: bpy.props.CollectionProperty(type=RR_isolate_collection_prop)
|
||||
|
||||
@property
|
||||
def render_dir_path(self):
|
||||
if not self.is_render_dir_valid:
|
||||
return None
|
||||
return Path(bpy.path.abspath(self.render_dir)).absolute()
|
||||
|
||||
@property
|
||||
def is_render_dir_valid(self) -> bool:
|
||||
if not self.render_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.render_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RR_property_group_sequence(bpy.types.PropertyGroup):
|
||||
"""
|
||||
Property group that will be registered on sequence strips.
|
||||
"""
|
||||
|
||||
is_render: bpy.props.BoolProperty(name="Is Render")
|
||||
is_approved: bpy.props.BoolProperty(name="Is Approved")
|
||||
is_pushed_to_edit: bpy.props.BoolProperty(name="Is Pushed To Edit")
|
||||
frames_found_text: bpy.props.StringProperty(name="Frames Found")
|
||||
shot_name: bpy.props.StringProperty(name="Shot")
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
RR_isolate_collection_prop,
|
||||
RR_property_group_scene,
|
||||
RR_property_group_sequence,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Scene Properties.
|
||||
bpy.types.Scene.rr = bpy.props.PointerProperty(
|
||||
name="Render Review",
|
||||
type=RR_property_group_scene,
|
||||
description="Metadata that is required for render_review",
|
||||
)
|
||||
|
||||
# Sequence Properties.
|
||||
bpy.types.Sequence.rr = bpy.props.PointerProperty(
|
||||
name="Render Review",
|
||||
type=RR_property_group_sequence,
|
||||
description="Metadata that is required for render_review",
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,178 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Set, Union, Optional, List, Dict, Any
|
||||
|
||||
import bpy
|
||||
|
||||
from .ops import (
|
||||
RR_OT_sqe_create_review_session,
|
||||
RR_OT_setup_review_workspace,
|
||||
RR_OT_sqe_inspect_exr_sequence,
|
||||
RR_OT_sqe_clear_exr_inspect,
|
||||
RR_OT_sqe_approve_render,
|
||||
RR_OT_sqe_update_sequence_statuses,
|
||||
RR_OT_open_path,
|
||||
RR_OT_sqe_push_to_edit,
|
||||
)
|
||||
from . import opsdata
|
||||
from .. import prefs
|
||||
|
||||
|
||||
class RR_PT_render_review(bpy.types.Panel):
|
||||
""" """
|
||||
|
||||
bl_category = "Render Review"
|
||||
bl_label = "Render Review"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 10
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
|
||||
# Label and setup workspace.
|
||||
row = box.row(align=True)
|
||||
row.label(text="Review", icon="CAMERA_DATA")
|
||||
row.operator(RR_OT_setup_review_workspace.bl_idname, text="", icon="WINDOW")
|
||||
|
||||
# Render dir prop.
|
||||
row = box.row(align=True)
|
||||
row.prop(context.scene.rr, "render_dir")
|
||||
|
||||
# Create session.
|
||||
render_dir = context.scene.rr.render_dir_path
|
||||
text = f"Invalid Render Directory"
|
||||
if render_dir:
|
||||
if opsdata.is_sequence_dir(render_dir):
|
||||
text = f"Review Sequence: {render_dir.name}"
|
||||
elif opsdata.is_shot_dir(render_dir):
|
||||
text = f"Review Shot: {render_dir.stem}"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_create_review_session.bl_idname, text=text, icon="PLAY")
|
||||
row = box.row(align=True)
|
||||
row.prop(addon_prefs, 'use_video')
|
||||
if addon_prefs.use_video:
|
||||
row.prop(addon_prefs, 'use_video_latest_only')
|
||||
|
||||
# Warning if kitsu on but not logged in.
|
||||
if not prefs.session_auth(context):
|
||||
row = box.split(align=True, factor=0.7)
|
||||
row.label(text="Kitsu enabled but not logged in", icon="ERROR")
|
||||
row.operator("kitsu.session_start", text="Login")
|
||||
|
||||
elif not opsdata.is_active_project():
|
||||
row = box.row(align=True)
|
||||
row.label(text="Kitsu enabled but no active project", icon="ERROR")
|
||||
|
||||
sqe = context.scene.sequence_editor
|
||||
if not sqe:
|
||||
return
|
||||
active_strip = sqe.active_strip
|
||||
if active_strip and active_strip.rr.is_render:
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF")
|
||||
box.separator()
|
||||
|
||||
# Render dir name label and open file op.
|
||||
row = box.row(align=True)
|
||||
directory = opsdata.get_strip_folder(active_strip)
|
||||
row.label(text=f"Folder: {directory.name}")
|
||||
row.operator(
|
||||
RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="", emboss=False
|
||||
).filepath = bpy.path.abspath(directory.as_posix())
|
||||
|
||||
# Nr of frames.
|
||||
box.row(align=True).label(text=f"Frames: {active_strip.rr.frames_found_text}")
|
||||
|
||||
# Inspect exr.
|
||||
text = "Inspect EXR"
|
||||
icon = "VIEWZOOM"
|
||||
if not opsdata.get_image_editor(context):
|
||||
text = "Inspect EXR: Needs Image Editor"
|
||||
icon = "ERROR"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_inspect_exr_sequence.bl_idname, icon=icon, text=text)
|
||||
row.operator(RR_OT_sqe_clear_exr_inspect.bl_idname, text="", icon="X")
|
||||
|
||||
# Approve render & udpate approved.
|
||||
row = box.row(align=True)
|
||||
|
||||
text = "Push To Edit & Approve Render"
|
||||
if active_strip.rr.is_pushed_to_edit:
|
||||
text = "Approve Render"
|
||||
row.operator(RR_OT_sqe_approve_render.bl_idname, icon="CHECKMARK", text=text)
|
||||
row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH")
|
||||
|
||||
# Push to edit.
|
||||
if not addon_prefs.shot_playblast_root_dir:
|
||||
shot_previews_dir = "" # ops handle invalid path
|
||||
else:
|
||||
shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix()
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_push_to_edit.bl_idname, icon="EXPORT")
|
||||
row.operator(RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="").filepath = (
|
||||
shot_previews_dir
|
||||
)
|
||||
|
||||
# Push strip to Kitsu.
|
||||
box.row().operator('kitsu.sqe_push_shot', icon='URL')
|
||||
|
||||
|
||||
def RR_topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
op = layout.operator(RR_OT_setup_review_workspace.bl_idname, text="Render Review")
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
RR_PT_render_review,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Append to topbar file new.
|
||||
bpy.types.TOPBAR_MT_file_new.append(RR_topbar_file_new_draw_handler)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
# Remove to topbar file new.
|
||||
bpy.types.TOPBAR_MT_file_new.remove(RR_topbar_file_new_draw_handler)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,44 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
import re
|
||||
from typing import Union, Dict, List, Any
|
||||
import bpy
|
||||
from . import vars
|
||||
|
||||
|
||||
def redraw_ui() -> None:
|
||||
"""
|
||||
Forces blender to redraw the UI.
|
||||
"""
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def get_version(str_value: str, format: type = str) -> Union[str, int, None]:
|
||||
match = re.search(vars.VERSION_PATTERN, str_value)
|
||||
if match:
|
||||
version = match.group()
|
||||
if format == str:
|
||||
return version
|
||||
if format == int:
|
||||
return int(version.replace("v", ""))
|
||||
return None
|
@ -1,25 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation - Paul Golter
|
||||
|
||||
# These defaults will be overridden if a Kitsu project is referenced
|
||||
RESOLUTION = (2048, 858)
|
||||
VERSION_PATTERN = r"v\d\d\d"
|
||||
FPS = 24
|
||||
DELIMITER = "-"
|
Loading…
Reference in New Issue
Block a user