Skip to content

Instantly share code, notes, and snippets.

@p2or
Last active May 21, 2023 19:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save p2or/cff74514f467034a03177871dbc8912b to your computer and use it in GitHub Desktop.
Save p2or/cff74514f467034a03177871dbc8912b to your computer and use it in GitHub Desktop.
Render Version #BlenderAddon
# ##### 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 #####
bl_info = {
"name": "Render Version",
"description": "Output version number control",
"author": "p2or",
"version": (0, 0, 2),
"blender": (2, 80, 0),
"location": "Properties > Output > Render Version",
"warning": "", # used for warning icon and text in addons panel
"wiki_url": "",
"tracker_url": "",
"category": "Render"
}
import bpy
import os
import re
from sys import platform
import webbrowser
# ------------------------------------------------------------------------
# Scene Properties
# ------------------------------------------------------------------------
def rv_update(self, context):
context.area.tag_redraw()
scene = context.scene
render = scene.render
# Replace the render path
render.filepath = version_number(
render.filepath,
scene.rv.rdr_ver
)
# Replace file output
if not scene.render.use_compositing or \
not scene.rv.sync_comp or \
not scene.node_tree:
return
nodes = scene.node_tree.nodes
output_nodes = [n for n in nodes if n.type=='OUTPUT_FILE']
for out_node in output_nodes:
if "LAYER" in out_node.format.file_format:
out_node.base_path = version_number(
out_node.base_path,
scene.rv.rdr_ver)
else:
for out_file in out_node.file_slots:
out_file.path = version_number(
out_file.path,
scene.rv.rdr_ver)
return None
class RVSettings(bpy.types.PropertyGroup):
rdr_ver : bpy.props.IntProperty(
name = "Render Version",
description="Render Version",
default = 1,
min = 1,
update = rv_update)
sync_comp : bpy.props.BoolProperty(
name = "Sync Compositor",
description="Sync version string with File Output nodes",
default = True)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class RVOpenOutputDirectory(bpy.types.Operator):
"""Open up Output Directory in File Browser"""
bl_idname = "rv.output_directory"
bl_label = "Open Output Directory in File Browser"
bl_description = "Output Directory"
bl_options = {'INTERNAL'}
def execute(self, context):
fp = os.path.dirname(context.scene.render.frame_path())
try:
if platform.startswith('darwin'):
webbrowser.open("file://{}".format(fp))
else:
webbrowser.open(fp)
except OSError:
self.report({'INFO'}, "No Folder")
return {'FINISHED'}
class RVOutNodesCleanup(bpy.types.Operator):
"""Remove version strings from File Output Nodes"""
bl_idname = "rv.remove_version_strings"
bl_label = "Remove Version Strings from File Output Nodes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == 'NODE_EDITOR'
def remove_version(self, fpath):
match = re.search(r'(v\d+)', fpath)
delimiters = ("-", "_", ".")
if match:
head, tail = fpath.split(match.group(0))
if tail.startswith(delimiters):
tail = tail[1:]
fpath = head + tail
return fpath[:-1] if fpath.endswith(delimiters) else fpath
else:
return fpath
def execute(self, context):
scene = context.scene
nodes = scene.node_tree.nodes
output_nodes = [n for n in nodes if n.type=='OUTPUT_FILE']
if not output_nodes:
self.report({'INFO'}, "Nothing to operate on")
return {'CANCELLED'}
for out_node in output_nodes:
if "LAYER" in out_node.format.file_format:
out_node.base_path = self.remove_version(out_node.base_path)
for layer in out_node.layer_slots:
layer.name = self.remove_version(layer.name)
else:
out_node.base_path = self.remove_version(out_node.base_path)
for out_file in out_node.file_slots:
out_file.path = self.remove_version(out_file.path)
scene.rv.sync_comp=False
return {'FINISHED'}
# ------------------------------------------------------------------------
# Drawing
# ------------------------------------------------------------------------
def draw_rv(self, context):
"""Append Properties and Operators to the Output Area"""
rv = context.scene.rv
if re.search("v\d+", context.scene.render.filepath) is not None:
layout = self.layout
#col = layout.column()
row = layout.row(align=True)
row.prop(rv, "rdr_ver") #NODE_COMPOSITING
row.prop(rv, "sync_comp", text="", toggle=True, icon="IMAGE_RGB_ALPHA")
row.operator(RVOpenOutputDirectory.bl_idname, icon="DISK_DRIVE", text="")
# ------------------------------------------------------------------------
# Helper
# ------------------------------------------------------------------------
def version_number(file_path, number, delimiter="_", min_lead=2):
"""Replace or add a version string by given number"""
match = re.search(r'v(\d+)', file_path)
if match:
g = match.group(1)
n = str(int(number)).zfill(len(g))
return file_path.replace(match.group(0), "v{v}".format(v=n))
else:
lead_zeros = str(int(number)).zfill(min_lead)
version = "{dl}v{lz}{dl}".format(dl=delimiter, lz=lead_zeros)
ext = (".png",".jpg",".jpeg","jpg",".exr",".dpx",".tga",".tif",".tiff",".cin")
if "#" in file_path:
dash = file_path.find("#")
head, tail = file_path[:dash], file_path[dash:]
if head.endswith(delimiter):
head = head.rstrip(delimiter)
return "{h}{v}{t}".format(h=head, v=version, t=tail)
elif file_path.endswith(ext):
head, extension = os.path.splitext(file_path)
if head.endswith(delimiter):
head = head.rstrip(delimiter)
return "{fp}{v}{ex}".format(fp=head, v=version[:-1], ex=extension)
else:
if file_path.endswith(delimiter):
file_path = file_path.rstrip(delimiter)
return "{fp}{v}".format(fp=file_path, v=version)
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
RVSettings,
RVOpenOutputDirectory,
RVOutNodesCleanup,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.rv = bpy.props.PointerProperty(type=RVSettings)
bpy.types.RENDER_PT_output.append(draw_rv)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
bpy.types.RENDER_PT_output.remove(draw_rv)
del bpy.types.Scene.rv
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment