save startup blend for animation tab & whatnot
This commit is contained in:
2026-04-08 12:10:18 -06:00
parent 57a652524a
commit 692e200ffe
180 changed files with 12336 additions and 3431 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -31,6 +31,7 @@ if "bpy" in locals():
importlib.reload(add_mesh_menger_sponge)
importlib.reload(add_mesh_vertex)
importlib.reload(add_empty_as_parent)
importlib.reload(add_mesh_equilateral_grid)
importlib.reload(add_mesh_beam_builder)
importlib.reload(Blocks)
importlib.reload(Wallfactory)
@@ -59,6 +60,7 @@ else:
from . import Wallfactory
from . import add_mesh_triangles
from . import preferences
from . import add_mesh_equilateral_grid
from .add_mesh_rocks import __init__
from .add_mesh_rocks import rockgen
@@ -154,6 +156,7 @@ class VIEW3D_MT_mesh_extras_add(Menu):
oper.change = False
oper = layout.operator("mesh.primitive_teapot_add", text="Teapot+")
oper = layout.operator("mesh.menger_sponge_add", text="Menger Sponge")
oper = layout.operator("mesh.add_equilateral_grid", text="Equilateral Grid")
class VIEW3D_MT_mesh_torus_add(Menu):
@@ -209,7 +212,7 @@ def menu_func(self, context):
if prefs.show_single_vert:
layout.menu("VIEW3D_MT_mesh_vert_add", text="Single Vert", icon='DECORATE')
if prefs.show_torus_objects:
layout.menu("VIEW3D_MT_mesh_torus_add", text="Torus Objects", icon='MESH_TORUS')
@@ -218,7 +221,7 @@ def menu_func(self, context):
if prefs.show_gears:
layout.menu("VIEW3D_MT_mesh_gears_add", text="Gears", icon='PREFERENCES')
if prefs.show_pipe_joints:
layout.menu("VIEW3D_MT_mesh_pipe_joints_add", text="Pipe Joints", icon='IPO_CONSTANT')
@@ -409,6 +412,7 @@ classes = [
add_mesh_menger_sponge.AddMengerSponge,
add_mesh_vertex.AddVert,
add_mesh_vertex.AddEmptyVert,
add_mesh_equilateral_grid.MESH_OT_add_equilateral_grid,
add_mesh_vertex.AddSymmetricalEmpty,
add_mesh_vertex.AddSymmetricalVert,
add_empty_as_parent.P2E,
@@ -0,0 +1,270 @@
# SPDX-FileCopyrightText: 2026 Blender Foundation
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Author: Luis-Lerga
import bpy
import bmesh
from math import sqrt
from bpy_extras import object_utils
class MESH_OT_add_equilateral_grid(bpy.types.Operator):
"""Add an equilateral triangular grid with hexagonal pattern"""
bl_idname = "mesh.add_equilateral_grid"
bl_label = "Add Equilateral Grid"
bl_options = {'REGISTER', 'UNDO'}
# Dimensions
width: bpy.props.FloatProperty(
name="Width",
description="Horizontal width of the rectangle",
default=10.0,
min=0.1,
max=100.0,
step=100,
precision=2,
unit='LENGTH'
)
height: bpy.props.FloatProperty(
name="Height",
description="Vertical height of the rectangle",
default=10.0,
min=0.1,
max=100.0,
step=100,
precision=2,
unit='LENGTH'
)
density: bpy.props.IntProperty(
name="Density",
description="Number of horizontal segments",
default=20,
min=4,
max=200,
step=1
)
# UVs
use_uv: bpy.props.BoolProperty(
name="Generate UVs",
description="Generate UV coordinates for the mesh",
default=True
)
# Alignment
align: bpy.props.EnumProperty(
name="Align",
description="Mesh alignment orientation",
items=[
('WORLD', "World", "Align to world origin"),
('VIEW', "View", "Align to current view"),
('CURSOR', "3D Cursor", "Align to 3D cursor position"),
],
default='WORLD'
)
# Transformations
location: bpy.props.FloatVectorProperty(
name="Location",
description="Object location",
subtype='TRANSLATION',
default=(0.0, 0.0, 0.0),
unit='LENGTH'
)
rotation: bpy.props.FloatVectorProperty(
name="Rotation",
description="Object rotation (Euler XYZ)",
subtype='EULER',
default=(0.0, 0.0, 0.0),
unit='ROTATION'
)
def execute(self, context):
# Create geometry & mesh.
verts, faces = self.create_geometry(context)
mesh = bpy.data.meshes.new("Equilateral Grid")
mesh.from_pydata(verts, [], faces)
mesh.update()
# Clean-up.
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(mesh)
bm.free()
# Generate UVs.
if self.use_uv:
self.generate_uvs(mesh)
# Create an object.
obj = object_utils.object_data_add(context, mesh, operator=self)
if context.preferences.edit.use_enter_edit_mode:
bpy.ops.object.mode_set(mode = 'EDIT')
return {'FINISHED'}
def create_geometry(self, context):
"""Generate geometry with equilateral triangles and perfect rectangular borders"""
L = self.width / self.density
tri_height = L * sqrt(3) / 2
# Calculate EVEN number of triangle rows for straight top/bottom borders
target_rows = self.height / tri_height
rows = int(round(target_rows / 2.0)) * 2 # Always even
rows = max(2, rows)
# Actual adjusted dimensions
actual_width = self.density * L
actual_height = rows * tri_height
# === Generate vertices WITHOUT protruding vertices ===
verts = []
row_offsets = [] # Track starting index of each row
for row in range(rows + 1): # rows+1 vertex rows
y = -actual_height / 2 + row * tri_height
row_offsets.append(len(verts))
if row % 2 == 0:
# Even rows: density+1 vertices spanning full width
num_verts = self.density + 1
offset = 0.0
else:
# Odd rows: density vertices (NO protruding vertex)
num_verts = self.density
offset = L * 0.5
for col in range(num_verts):
x = -actual_width / 2 + col * L + offset
verts.append((x, y, 0.0))
# === Generate interior faces (equilateral triangles) ===
faces = []
for row in range(rows):
if row % 2 == 0:
# Even row → next row is odd (fewer vertices)
for col in range(self.density):
i00 = row_offsets[row] + col
i01 = i00 + 1
i10 = row_offsets[row + 1] + col
i11 = i10 + 1 if col < self.density - 1 else None
faces.append((i00, i10, i01))
if i11 is not None:
faces.append((i01, i10, i11))
else:
# Odd row → next row is even (more vertices)
for col in range(self.density):
i00 = row_offsets[row] + col
i01 = i00 + 1 if col < self.density - 1 else None
i10 = row_offsets[row + 1] + col
i11 = i10 + 1
if i01 is not None:
faces.append((i00, i11, i01))
faces.append((i00, i10, i11))
# === Fill LEFT/RIGHT borders for odd rows ===
for row in range(1, rows, 2): # Only odd rows (1, 3, 5...)
y = -actual_height / 2 + row * tri_height
# Add LEFT border vertex at exact x = -width/2
left_idx = len(verts)
verts.append((-actual_width / 2, y, 0.0))
# Add RIGHT border vertex at exact x = +width/2
right_idx = len(verts)
verts.append((actual_width / 2, y, 0.0))
# Connect LEFT border vertex
base_idx = row_offsets[row]
above_idx = row_offsets[row - 1] # Even row above
below_idx = row_offsets[row + 1] # Even row below
# Upper triangle: left border → first interior → vertex above first interior
faces.append((left_idx, base_idx, above_idx))
# Lower triangle: left border → vertex below first interior → first interior
faces.append((left_idx, below_idx, base_idx))
# Connect RIGHT border vertex
last_interior = base_idx + self.density - 1
above_last = above_idx + self.density # Last vertex of even row above
below_last = below_idx + self.density # Last vertex of even row below
# Upper triangle: right border → last interior → vertex above last interior
faces.append((right_idx, last_interior, above_last))
# Lower triangle: right border → vertex below last interior → last interior
faces.append((right_idx, below_last, last_interior))
return verts, faces
def generate_uvs(self, mesh):
"""Generate simple 0-1 UV coordinates based on X,Y coordinates"""
if not mesh.uv_layers:
mesh.uv_layers.new(name="UVMap")
uv_layer = mesh.uv_layers.active.data
# Calculate bounds for normalization
xs = [v.co.x for v in mesh.vertices]
ys = [v.co.y for v in mesh.vertices]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
width = max_x - min_x or 1.0
height = max_y - min_y or 1.0
# Assign UVs
for poly in mesh.polygons:
for loop_idx in poly.loop_indices:
vert_idx = mesh.loops[loop_idx].vertex_index
v = mesh.vertices[vert_idx].co
uv = (
(v.x - min_x) / width,
(v.y - min_y) / height
)
uv_layer[loop_idx].uv = uv
def draw(self, context):
"""Draw the options panel in the dialog"""
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
# Dimensions
layout.label(text="Dimensions:")
col = layout.column(align=True)
col.prop(self, "width")
col.prop(self, "height")
col.prop(self, "density")
# Additional options
layout.separator()
layout.prop(self, "use_uv")
layout.prop(self, "align")
# Transformations
layout.separator()
col = layout.column(align=True)
col.prop(self, "location", text="Location X", index=0)
col.prop(self, "location", text="Y", index=1)
col.prop(self, "location", text="Z", index=2)
col = layout.column(align=True)
col.prop(self, "rotation", text="Rotation X", index=0)
col.prop(self, "rotation", text="Y", index=1)
col.prop(self, "rotation", text="Z", index=2)
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "extra_mesh_objects"
name = "Extra Mesh Objects"
version = "0.4.0"
version = "0.4.1"
tagline = "Add extra mesh object types"
maintainer = "Community"
type = "add-on"
@@ -4,7 +4,7 @@ Documentation: https://weisl.github.io/renaming_overview/
<h1>Introduction</h1>
<p><b> Simple Renaming Panel </b> is a small, but powerful tool to rename more objects at once. The tool includes basic functionalities of adding suffixes, prefixes, search and replace, add suffixes depending on the object type and much more. Over the time more advanced features like a variable system were added. The tool gives a lot of power to you!
<p><b> Simple Renaming</b> is a small, but powerful tool to rename more objects at once. The tool includes basic functionalities of adding suffixes, prefixes, search and replace, add suffixes depending on the object type and much more. Over the time more advanced features like a variable system were added. The tool gives a lot of power to you!
You decide which kind of objects will be affected by the renaming task. Rename all or just selected objects, specify the affected object types like image textures, materials, objects, object data, bones, or collections. This tool can be a real everyday helper. Renaming multiple objects is often needed and keeping the naming conventions can be tedious. The tool provides you with a clear feedback of what has been renamed. This tool is kept simple to be user-friendly but offers everything you need to stay organized. </p>
Join the discussion at [Blender Artists](https://blenderartists.org/t/simple-renaming-panel/676639 "Blender Artists").
@@ -92,7 +92,8 @@ enumObjectTypesExt = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY',
('GPENCIL', "", "Rename greace pencil objects", 'OUTLINER_OB_GREASEPENCIL', 512),
('METABALL', "", "Rename metaball objects", 'OUTLINER_OB_META', 2048),
('COLLECTION', "", "Rename collections", 'GROUP', 4096),
('BONE', "", "", 'BONE_DATA', 8192), ]
('BONE', "", "", 'BONE_DATA', 8192),
('POINTCLOUD', "", "Rename point cloud objects", 'OUTLINER_OB_POINTCLOUD', 16384), ]
def register():
@@ -113,7 +114,8 @@ def register():
'MESH',
'ARMATURE', 'LIGHT', 'CAMERA', 'EMPTY',
'GPENCIL',
'TEXT', 'BONE', 'COLLECTION'}
'TEXT', 'BONE', 'COLLECTION',
'POINTCLOUD'}
)
id_store.renaming_suffix_prefix_material = StringProperty(name='Material', default='')
@@ -135,6 +137,7 @@ def register():
id_store.renaming_suffix_prefix_bone = StringProperty(name="Bones", default='')
id_store.renaming_suffix_prefix_speakers = StringProperty(name="Speakers", default='')
id_store.renaming_suffix_prefix_lightprops = StringProperty(name="LightProps", default='')
id_store.renaming_suffix_prefix_pointcloud = StringProperty(name="Point Cloud", default='')
id_store.renaming_inputContext = StringProperty(name="LightProps", default='')
@@ -40,9 +40,6 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
option: StringProperty()
def __init__(self):
self.context = None
def get_selection_all(self):
context = self.context
@@ -266,6 +263,18 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
icon='OUTLINER_OB_META')
return
def pointcloud(self):
context = self.context
wm = context.scene
obj_list = []
for obj in self.get_selection_all():
if obj.type == 'POINTCLOUD':
obj_list.append(obj)
self.rename_suffix_prefix(obj_list, pre_suffix=wm.renaming_suffix_prefix_pointcloud, object_type='POINTCLOUD',
icon='OUTLINER_OB_POINTCLOUD')
return
def collection(self):
context = self.context
wm = context.scene
@@ -303,6 +312,7 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
self.metaball()
self.collection()
self.bone()
self.pointcloud()
self.material()
self.data()
@@ -26,4 +26,5 @@ paths_exclude_pattern = [
"__pycache__/",
"/.git/",
"/*.zip",
"/tests/",
]
@@ -9,6 +9,9 @@ from bpy.props import (
)
from . import add_pre_suffix
from . import case_transform
from . import reload_addon
from .version_check import start_version_check
from . import name_from_data
from . import name_replace
from . import numerate
@@ -30,7 +33,8 @@ enumObjectTypes = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY', 1)
('META', "", "Rename metaball objects", 'OUTLINER_OB_META', 1024),
('SPEAKER', "", "Rename empty speakers", 'OUTLINER_OB_SPEAKER', 2048),
('LIGHT_PROBE', "", "Rename mesh lightpropes", 'OUTLINER_OB_LIGHTPROBE', 4096),
('VOLUME', "", "Rename mesh volumes", 'OUTLINER_OB_VOLUME', 8192)]
('VOLUME', "", "Rename mesh volumes", 'OUTLINER_OB_VOLUME', 8192),
('POINTCLOUD', "", "Rename point cloud objects", 'OUTLINER_OB_POINTCLOUD', 16384)]
enumObjectTypesAdd = [('SPEAKER', "", "Rename empty speakers", 'OUTLINER_OB_SPEAKER', 1),
('LIGHT_PROBE', "", "Rename mesh lightpropes", 'OUTLINER_OB_LIGHTPROBE', 2)]
@@ -60,6 +64,8 @@ renamingEntitiesItems = [('OBJECT', "Object", "Scene Objects"),
None,
('PARTICLESYSTEM', "Particle Systems", "Rename particle systems"),
('PARTICLESETTINGS', "Particle Settings", "Rename particle settings"),
None,
('NODE_GROUPS', "Node Groups", "Rename node groups"),
]
classes = (
@@ -71,6 +77,13 @@ classes = (
add_pre_suffix.VIEW3D_OT_add_prefix,
numerate.VIEW3D_OT_renaming_numerate,
name_from_data.VIEW3D_OT_use_objectname_for_data,
case_transform.VIEW3D_OT_case_upper,
case_transform.VIEW3D_OT_case_lower,
case_transform.VIEW3D_OT_case_pascal,
case_transform.VIEW3D_OT_case_camel,
case_transform.VIEW3D_OT_case_snake,
case_transform.VIEW3D_OT_case_kebab,
reload_addon.VIEW3D_OT_reload_addon,
)
enum_sort_items = [('X', "X Axis", "Sort the object based on the X axis."),
@@ -120,7 +133,8 @@ def register():
options={'ENUM_FLAG'},
default={'CURVE', 'LATTICE', 'SURFACE', 'MESH',
'ARMATURE', 'LIGHT', 'CAMERA', 'EMPTY', 'GPENCIL',
'FONT', 'SPEAKER', 'LIGHT_PROBE', 'VOLUME'}
'FONT', 'SPEAKER', 'LIGHT_PROBE', 'VOLUME',
'POINTCLOUD'}
)
id_store.renaming_sort_enum = EnumProperty(
@@ -166,12 +180,31 @@ def register():
id_store.renaming_digits_numerate = IntProperty(name="Number Length", default=3)
id_store.renaming_trim_indices = IntVectorProperty(name="Trim Size", default=(0, 0), min=0, soft_min=0, size=2)
id_store.renaming_active_only = BoolProperty(
name="Active Only",
description="Only rename the active layer on each object",
default=False,
)
id_store.renaming_filter_by_index = BoolProperty(
name="By Index",
description="Only rename the layer at the specified index on each object",
default=False,
)
id_store.renaming_index_target = IntProperty(name="Index", default=0, min=0)
id_store.renaming_also_rename_data = BoolProperty(
name="Also Rename Data",
description="Also rename the linked data block (mesh, curve, etc.) to match the object name",
default=False,
)
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(PostChange)
start_version_check()
def unregister():
@@ -192,5 +225,9 @@ def unregister():
del IDStore.renaming_base_numerate
del IDStore.renaming_digits_numerate
del IDStore.renaming_trim_indices
del IDStore.renaming_also_rename_data
del IDStore.renaming_active_only
del IDStore.renaming_filter_by_index
del IDStore.renaming_index_target
bpy.app.handlers.depsgraph_update_post.remove(PostChange)
@@ -1,7 +1,9 @@
import time
import bpy
from .renaming_operators import switch_to_edit_mode
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
from ..variable_replacer.variable_replacer import VariableReplacer
@@ -23,9 +25,11 @@ class VIEW3D_OT_add_suffix(bpy.types.Operator):
call_error_popup(context)
return {'CANCELLED'}
t_start = time.perf_counter()
msg = wm.renaming_messages
VariableReplacer.reset()
VariableReplacer.prepare(context)
if len(renaming_list) > 0:
for entity in renaming_list:
if entity is not None:
@@ -34,11 +38,15 @@ class VIEW3D_OT_add_suffix(bpy.types.Operator):
oldName = entity.name
new_name = entity.name + suffix
entity.name = new_name
rename_data_if_enabled(wm, entity)
if wm.renaming_object_types == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
else:
msg.add_message(None, None, "Insert Valid String")
if switch_edit_mode:
switch_to_edit_mode(context)
log_timing(context, "add_suffix", t_start, len(renaming_list))
call_renaming_popup(context)
return {'FINISHED'}
@@ -62,7 +70,9 @@ class VIEW3D_OT_add_prefix(bpy.types.Operator):
call_error_popup(context)
return {'CANCELLED'}
t_start = time.perf_counter()
VariableReplacer.reset()
VariableReplacer.prepare(context)
if len(renaming_list) > 0:
for entity in renaming_list:
@@ -72,8 +82,12 @@ class VIEW3D_OT_add_prefix(bpy.types.Operator):
oldName = entity.name
new_name = pre + entity.name
entity.name = new_name
rename_data_if_enabled(wm, entity)
if wm.renaming_object_types == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
log_timing(context, "add_prefix", t_start, len(renaming_list))
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
@@ -0,0 +1,159 @@
import re
import bpy
from .renaming_operators import switch_to_edit_mode
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers
# ---------------------------------------------------------------------------
# Word splitting — handles snake_case, kebab-case, PascalCase, camelCase
# ---------------------------------------------------------------------------
def split_words(name):
"""Split a name into words regardless of input convention."""
# camelCase / PascalCase boundaries: lowercase→Uppercase
name = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', name)
# Runs of capitals before a capitalised word: XMLParser → XML Parser
name = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1 \2', name)
# Replace separators with spaces
name = re.sub(r'[-_\s]+', ' ', name)
return [w for w in name.split(' ') if w]
# ---------------------------------------------------------------------------
# Conversion helpers (also imported by search_replace for \u \l \U \L)
# ---------------------------------------------------------------------------
def to_upper(text):
return text.upper()
def to_lower(text):
return text.lower()
def upper_first(text):
"""Uppercase only the first character, leave the rest unchanged."""
return text[:1].upper() + text[1:] if text else text
def lower_first(text):
"""Lowercase only the first character, leave the rest unchanged."""
return text[:1].lower() + text[1:] if text else text
def to_pascal_case(name):
"""hello_world → HelloWorld"""
return ''.join(w.capitalize() for w in split_words(name))
def to_camel_case(name):
"""hello_world → helloWorld"""
words = split_words(name)
if not words:
return name
return words[0].lower() + ''.join(w.capitalize() for w in words[1:])
def to_snake_case(name):
"""HelloWorld → hello_world"""
return '_'.join(w.lower() for w in split_words(name))
def to_kebab_case(name):
"""HelloWorld → hello-world"""
return '-'.join(w.lower() for w in split_words(name))
# ---------------------------------------------------------------------------
# Operator base
# ---------------------------------------------------------------------------
class _CaseOperatorBase(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def _transform(self, name):
raise NotImplementedError
def execute(self, context):
scene = context.scene
renaming_list, switch_edit_mode, errMsg = get_renaming_list(context)
if errMsg is not None:
scene.renaming_error_messages.add_message(errMsg)
call_error_popup(context)
return {'CANCELLED'}
msg = scene.renaming_messages
for entity in renaming_list:
if entity is not None:
old_name = entity.name
entity.name = self._transform(entity.name)
rename_data_if_enabled(scene, entity)
if scene.renaming_object_types == 'BONE':
update_bone_drivers(old_name, entity.name)
msg.add_message(old_name, entity.name)
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------
class VIEW3D_OT_case_upper(_CaseOperatorBase):
bl_idname = "renaming.case_upper"
bl_label = "UPPERCASE"
bl_description = "Convert name to UPPERCASE (hello_world → HELLO_WORLD)"
def _transform(self, name):
return to_upper(name)
class VIEW3D_OT_case_lower(_CaseOperatorBase):
bl_idname = "renaming.case_lower"
bl_label = "lowercase"
bl_description = "Convert name to lowercase (Hello_World → hello_world)"
def _transform(self, name):
return to_lower(name)
class VIEW3D_OT_case_pascal(_CaseOperatorBase):
bl_idname = "renaming.case_pascal"
bl_label = "PascalCase"
bl_description = "Convert name to PascalCase (hello_world → HelloWorld)"
def _transform(self, name):
return to_pascal_case(name)
class VIEW3D_OT_case_camel(_CaseOperatorBase):
bl_idname = "renaming.case_camel"
bl_label = "camelCase"
bl_description = "Convert name to camelCase (hello_world → helloWorld)"
def _transform(self, name):
return to_camel_case(name)
class VIEW3D_OT_case_snake(_CaseOperatorBase):
bl_idname = "renaming.case_snake"
bl_label = "snake_case"
bl_description = "Convert name to snake_case (HelloWorld → hello_world)"
def _transform(self, name):
return to_snake_case(name)
class VIEW3D_OT_case_kebab(_CaseOperatorBase):
bl_idname = "renaming.case_kebab"
bl_label = "kebab-case"
bl_description = "Convert name to kebab-case (HelloWorld → hello-world)"
def _transform(self, name):
return to_kebab_case(name)
@@ -1,9 +1,11 @@
import time
import bpy
from .renaming_operators import getAllVertexGroups, getAllAttributes, getAllBones, getAllModifiers, getAllUvMaps, \
getAllColorAttributes, getAllParticleNames, getAllParticleSettingsNames, getAllDataNames, getAllShapeKeys
from .renaming_operators import getAllModifiers, \
getAllParticleNames, getAllParticleSettingsNames, getAllDataNames
from .renaming_operators import switch_to_edit_mode, numerate_entity_name
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
from ..variable_replacer.variable_replacer import VariableReplacer
@@ -27,57 +29,67 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
return {'CANCELLED'}
old_mode = context.mode
t_start = time.perf_counter()
# settings for numerating the new name
msg = scene.renaming_messages
vertexGroupNameList = []
particleSettingsList = []
particleList = []
uvmapsList = []
dataList = []
attributeList = []
colorAttributeList = []
shapeKeyNamesList = []
modifierNamesList = []
boneList = []
per_object_types = {'SHAPEKEYS', 'VERTEXGROUPS', 'UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'BONE'}
per_obj_owner_items = {
'SHAPEKEYS': lambda o: o.key_blocks,
'VERTEXGROUPS': lambda o: o.vertex_groups,
'UVMAPS': lambda o: o.uv_layers,
'COLORATTRIBUTES': lambda o: o.color_attributes,
'ATTRIBUTES': lambda o: o.attributes,
'BONE': lambda o: o.edit_bones if old_mode == 'EDIT_ARMATURE' else o.bones,
}
particleSettingsList = set()
particleList = set()
dataList = set()
modifierNamesList = set()
if context.scene.renaming_object_types == 'VERTEXGROUPS':
vertexGroupNameList = getAllVertexGroups()
if scene.renaming_object_types == 'PARTICLESYSTEM':
particleList = getAllParticleNames()
particleList = set(getAllParticleNames())
if scene.renaming_object_types == 'PARTICLESETTINGS':
particleSettingsList = getAllParticleSettingsNames()
if context.scene.renaming_object_types == 'UVMAPS':
uvmapsList = getAllUvMaps()
if context.scene.renaming_object_types == 'COLORATTRIBUTES':
colorAttributeList = getAllColorAttributes()
if context.scene.renaming_object_types == 'ATTRIBUTES':
attributeList = getAllAttributes()
if scene.renaming_object_types == 'SHAPEKEYS':
shapeKeyNamesList = getAllShapeKeys()
particleSettingsList = set(getAllParticleSettingsNames())
if scene.renaming_object_types == 'MODIFIERS':
modifierNamesList = getAllModifiers()
if scene.renaming_object_types == 'BONE':
boneList = getAllBones(old_mode)
modifierNamesList = set(getAllModifiers())
if scene.renaming_object_types == 'DATA':
dataList = getAllDataNames()
dataList = set(getAllDataNames())
current_owner = None
per_obj_name_list = set()
VariableReplacer.reset()
VariableReplacer.prepare(context)
if len(str(replaceName)) > 0: # New name != empty
if len(renaming_list) > 0: # List of objects to rename != empty
for entity in renaming_list:
if entity is not None:
if scene.renaming_object_types in per_object_types:
owner = entity.id_data
if owner != current_owner:
current_owner = owner
VariableReplacer.reset()
per_obj_name_list = {item.name for item in per_obj_owner_items[scene.renaming_object_types](owner)}
replaceName = VariableReplacer.replaceInputString(context, scene.renaming_new_name, entity)
oldName = entity.name
new_name = ''
if not scene.renaming_use_enumerate:
entity.name = replaceName
msg.add_message(oldName, entity.name)
try:
entity.name = replaceName
rename_data_if_enabled(scene, entity)
if scene.renaming_object_types == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
except AttributeError:
print("Attribute {} is read only".format(replaceName))
else: # if scene.renaming_use_enumerate == True
@@ -94,57 +106,37 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
new_name, dataList = numerate_entity_name(context, replaceName, dataList, entity.name,
return_type_list=True)
elif scene.renaming_object_types == 'BONE':
new_name, boneList = numerate_entity_name(context, replaceName, boneList, entity.name,
return_type_list=True)
elif scene.renaming_object_types == 'COLLECTION':
new_name = numerate_entity_name(context, replaceName, bpy.data.collections, entity.name)
elif scene.renaming_object_types == 'ACTIONS':
new_name = numerate_entity_name(context, replaceName, bpy.data.actions, entity.name)
elif scene.renaming_object_types == 'SHAPEKEYS':
new_name, shapeKeyNamesList = numerate_entity_name(context, replaceName,
shapeKeyNamesList, entity.name,
elif scene.renaming_object_types in per_object_types:
new_name, per_obj_name_list = numerate_entity_name(context, replaceName,
per_obj_name_list, entity.name,
return_type_list=True)
elif scene.renaming_object_types == 'MODIFIERS':
new_name, modifierNamesList = numerate_entity_name(context, replaceName,
modifierNamesList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'VERTEXGROUPS':
new_name, vertexGroupNameList = numerate_entity_name(context, replaceName,
vertexGroupNameList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'PARTICLESYSTEM':
elif scene.renaming_object_types == 'PARTICLESYSTEM':
new_name, particleList = numerate_entity_name(context, replaceName,
particleList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'PARTICLESETTINGS':
elif scene.renaming_object_types == 'PARTICLESETTINGS':
new_name, particleSettingsList = numerate_entity_name(context, replaceName,
particleSettingsList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'UVMAPS':
new_name, uvmapsList = numerate_entity_name(context, replaceName,
uvmapsList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'ATTRIBUTES':
new_name, attributeList = numerate_entity_name(context, replaceName,
attributeList, entity.name,
return_type_list=True)
elif context.scene.renaming_object_types == 'COLORATTRIBUTES':
new_name, colorAttributeList = numerate_entity_name(context, replaceName,
colorAttributeList, entity.name,
return_type_list=True)
try:
entity.name = new_name
rename_data_if_enabled(scene, entity)
if scene.renaming_object_types == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
except AttributeError:
print("Attribute {} is read only".format(new_name))
@@ -153,6 +145,7 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
else: # len(str(replaceName)) <= 0
msg.add_message(None, None, "Insert a valid string to replace names")
log_timing(context, "name_replace", t_start, len(renaming_list))
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
@@ -1,8 +1,10 @@
import time
import bpy
from .renaming_operators import switch_to_edit_mode
from .. import __package__ as base_package
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
class VIEW3D_OT_renaming_numerate(bpy.types.Operator):
@@ -32,17 +34,31 @@ class VIEW3D_OT_renaming_numerate(bpy.types.Operator):
call_error_popup(context)
return {'CANCELLED'}
per_object_types = {'SHAPEKEYS', 'VERTEXGROUPS', 'UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'BONE'}
obj_type = wm.renaming_object_types
t_start = time.perf_counter()
if len(renaming_list) > 0:
i = 0
current_owner = None
for entity in renaming_list:
if entity is not None:
if obj_type in per_object_types:
owner = entity.id_data
if owner != current_owner:
current_owner = owner
i = 0
oldName = entity.name
new_name = entity.name + separator + (
'{num:{fill}{width}}'.format(num=(i * step) + start_number, fill='0', width=digits))
entity.name = new_name
rename_data_if_enabled(wm, entity)
if obj_type == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
i = i + 1
log_timing(context, "numerate", t_start, len(renaming_list))
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
@@ -0,0 +1,61 @@
import bpy
from bpy.types import Operator
class VIEW3D_OT_reload_addon(Operator):
"""Reload all Simple Renaming scripts."""
bl_idname = "renaming.reload_addon"
bl_label = "Reload Addon"
bl_description = "Reload all Simple Renaming scripts"
def execute(self, context):
import importlib
import sys
# Derive the addon root package name by stripping the sub-package suffix.
# Works for both legacy addons ("simple_renaming.operators")
# and extensions ("bl_ext.user_default.simple_renaming.operators").
root_pkg = __package__.rsplit(".", 1)[0]
# Snapshot the module names now, before any reload happens.
# Sort key: deeper modules first (so core.* sub-modules reload before
# core.__init__), and alphabetically within the same depth so that
# "core.*" always reloads before "operators.*" before "ui.*".
mod_names = sorted(
[name for name in sys.modules
if name == root_pkg or name.startswith(root_pkg + ".")],
key=lambda n: (-n.count("."), n),
)
# Defer the actual reload to the next event-loop iteration so that this
# operator's own execute() has finished (and its class has been removed
# from the call stack) before we unregister and reload everything.
def _do_reload():
root_mod = sys.modules.get(root_pkg)
if root_mod and hasattr(root_mod, "unregister"):
try:
root_mod.unregister()
except Exception as exc:
print(f"[RENAMING] unregister error: {exc}")
for name in mod_names:
mod = sys.modules.get(name)
if mod is not None:
try:
importlib.reload(mod)
except Exception as exc:
print(f"[RENAMING] reload error for '{name}': {exc}")
# Re-fetch root after in-place reload to pick up any top-level changes.
root_mod = sys.modules.get(root_pkg)
if root_mod and hasattr(root_mod, "register"):
try:
root_mod.register()
except Exception as exc:
print(f"[RENAMING] register error: {exc}")
print(f"[RENAMING] Reloaded {len(mod_names)} modules from '{root_pkg}'")
bpy.app.timers.register(_do_reload, first_interval=0.0)
self.report({'INFO'}, f"Queued reload of {len(mod_names)} modules…")
return {'FINISHED'}
@@ -0,0 +1,62 @@
import time
import bpy
from ..operators.renaming_utilities import call_renaming_popup, call_error_popup, log_timing
INDEXED_TYPES = ('UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'VERTEXGROUPS', 'SHAPEKEYS')
_accessor_map = {
'UVMAPS': lambda obj: obj.data.uv_layers,
'COLORATTRIBUTES': lambda obj: obj.data.color_attributes,
'ATTRIBUTES': lambda obj: obj.data.attributes,
'VERTEXGROUPS': lambda obj: obj.vertex_groups,
'SHAPEKEYS': lambda obj: obj.data.shape_keys.key_blocks if obj.data and obj.data.shape_keys else [],
}
class VIEW3D_OT_rename_by_index(bpy.types.Operator):
bl_idname = "renaming.rename_by_index"
bl_label = "Rename Slot"
bl_description = "Rename the item at the specified index on each selected object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
target_index = scene.renaming_index_target
new_name = scene.renaming_index_new_name
entity_type = scene.renaming_object_types
msg = scene.renaming_messages
if not new_name:
error_msg = scene.renaming_error_messages
error_msg.add_message("Name field is empty")
call_error_popup(context)
return {'CANCELLED'}
get_collection = _accessor_map.get(entity_type)
if get_collection is None:
return {'CANCELLED'}
obj_list = context.selected_objects.copy() if scene.renaming_only_selection else list(bpy.data.objects)
t_start = time.perf_counter()
renamed = 0
for obj in obj_list:
if obj.type != 'MESH':
continue
try:
items = list(get_collection(obj))
if target_index < len(items):
item = items[target_index]
old_name = item.name
item.name = new_name
msg.add_message(old_name, item.name)
renamed += 1
except Exception as e:
self.report({'WARNING'}, f"Skipped {obj.name}: {e}")
continue
log_timing(context, "rename_by_index", t_start, renamed)
call_renaming_popup(context)
return {'FINISHED'}
@@ -29,120 +29,64 @@ def numerate_entity_name(context, basename, type_list, active_entity_name, retur
'{num:{fill}{width}}'.format(num=(i * step) + start_number, fill='0', width=digits))
i += 1
if return_type_list: # Manually add new name to custom generated list like all bones and all shape keys
type_list.append(new_name)
if return_type_list: # Manually add new name to custom generated set like all bones and all shape keys
type_list.add(new_name)
return new_name, type_list
return new_name
def getAllBones(mode):
"""Get list of all bones depending on Edit or Pose Mode"""
boneList = []
for arm in bpy.data.armatures:
if mode == 'POSE':
for bone in arm.bones:
boneList.append(bone.name)
else: # mode == 'EDIT':
for bone in arm.edit_bones:
boneList.append(bone.name)
return boneList
"""Get list of all bone names depending on Edit or Pose Mode"""
if mode == 'POSE':
return [bone.name for arm in bpy.data.armatures for bone in arm.bones]
else: # mode == 'EDIT'
return [bone.name for arm in bpy.data.armatures for bone in arm.edit_bones]
def getAllModifiers():
"""get list of all modifiers"""
modifierList = []
for obj in bpy.data.objects:
for mod in obj.modifiers:
modifierList.append(mod.name)
return modifierList
"""get list of all modifier names"""
return [mod.name for obj in bpy.data.objects for mod in obj.modifiers]
def getAllShapeKeys():
"""get list of all shape keys"""
shapeKeyNamesList = []
for key_grp in bpy.data.shape_keys:
for key in key_grp.key_blocks:
shapeKeyNamesList.append(key.name)
return shapeKeyNamesList
"""get list of all shape key names"""
return [key.name for key_grp in bpy.data.shape_keys for key in key_grp.key_blocks]
def getAllVertexGroups():
"""get list of all vertex groups"""
vrtx_grp_names_list = []
for obj in bpy.data.objects:
for vrtGrp in obj.vertex_groups:
vrtx_grp_names_list.append(vrtGrp.name)
return vrtx_grp_names_list
"""get list of all vertex group names"""
return [vg.name for obj in bpy.data.objects for vg in obj.vertex_groups]
def getAllParticleNames():
"""get list of all particle systems"""
particlesNamesList = []
for obj in bpy.data.objects:
for particle_system in obj.particle_systems:
particlesNamesList.append(particle_system.name)
return particlesNamesList
"""get list of all particle system names"""
return [ps.name for obj in bpy.data.objects for ps in obj.particle_systems]
def getAllParticleSettingsNames():
"""get list of all particle settings"""
particlesNamesList = []
for par in bpy.data.particles:
particlesNamesList.append(par.name)
return particlesNamesList
"""get list of all particle settings names"""
return [par.name for par in bpy.data.particles]
def getAllUvMaps():
uvNamesList = []
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
for uv in obj.data.uv_layers:
uvNamesList.append(uv)
return uvNamesList
"""get list of all UV map names"""
return [uv.name for obj in bpy.data.objects if obj.type == 'MESH'
for uv in obj.data.uv_layers]
def getAllColorAttributes():
colorAttributesList = []
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
for color_attribute in obj.data.color_attributes:
colorAttributesList.append(color_attribute)
return colorAttributesList
"""get list of all color attribute names"""
return [ca.name for obj in bpy.data.objects if obj.type == 'MESH'
for ca in obj.data.color_attributes]
def getAllAttributes():
attributesList = []
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
for color_attribute in obj.data.color_attributes:
attributesList.append(color_attribute)
return attributesList
"""get list of all attribute names"""
return [attr.name for obj in bpy.data.objects if obj.type == 'MESH'
for attr in obj.data.attributes]
def getAllDataNames():
"""get list of all data"""
dataList = []
for obj in bpy.data.objects:
if obj.data is not None:
dataList.append(obj.data.name)
return dataList
"""get list of all data names"""
return [obj.data.name for obj in bpy.data.objects if obj.data is not None]
@@ -1,9 +1,22 @@
import time
import bpy
from bpy.types import PoseBone, EditBone
from .. import __package__ as base_package
def log_timing(context, label, t_start, entity_count):
"""Print elapsed time to the console when debug_timing is enabled."""
prefs = context.preferences.addons[base_package].preferences
if not prefs.debug_timing:
return
elapsed_ms = (time.perf_counter() - t_start) * 1000
print(f"[RENAMING] {label}: {elapsed_ms:.1f} ms ({entity_count} entities, "
f"{elapsed_ms / entity_count:.3f} ms/entity)" if entity_count else
f"[RENAMING] {label}: {elapsed_ms:.1f} ms")
def trim_string(string, size):
return string[size[0]:max(0, len(string)-size[1])]
@@ -38,12 +51,14 @@ def get_renaming_list(context):
if scene.renaming_object_types == 'OBJECT':
for obj in obj_list:
if obj in obj_list and obj.type in scene.renaming_object_types_specified:
if obj.type in scene.renaming_object_types_specified:
renaming_list.append(obj)
elif scene.renaming_object_types == 'DATA':
seen_data = set()
for obj in obj_list:
if obj.data not in renaming_list:
if obj.data is not None and id(obj.data) not in seen_data:
seen_data.add(id(obj.data))
renaming_list.append(obj.data)
elif scene.renaming_object_types == 'MATERIAL':
@@ -117,14 +132,25 @@ def get_renaming_list(context):
renaming_list = list(bpy.data.collections)
elif scene.renaming_object_types == 'SHAPEKEYS':
filter_index = scene.renaming_filter_by_index
idx = scene.renaming_index_target
if selection_only:
for obj in context.selected_objects:
for shape in obj.data.shape_keys.key_blocks:
renaming_list.append(shape)
if obj.data and obj.data.shape_keys:
items = list(obj.data.shape_keys.key_blocks)
if filter_index:
if idx < len(items):
renaming_list.append(items[idx])
else:
renaming_list.extend(items)
else: # selection_only == False:
for key_grp in bpy.data.shape_keys:
for key in key_grp.key_blocks:
renaming_list.append(key)
items = list(key_grp.key_blocks)
if filter_index:
if idx < len(items):
renaming_list.append(items[idx])
else:
renaming_list.extend(items)
elif scene.renaming_object_types == 'MODIFIERS':
if selection_only:
@@ -137,14 +163,16 @@ def get_renaming_list(context):
renaming_list.append(mod)
elif context.scene.renaming_object_types == 'VERTEXGROUPS':
if selection_only:
for obj in context.selected_objects:
for vtx in obj.vertex_groups:
renaming_list.append(vtx)
else:
for obj in bpy.data.objects:
for vtx in obj.vertex_groups:
renaming_list.append(vtx)
filter_index = scene.renaming_filter_by_index
idx = scene.renaming_index_target
obj_iter = context.selected_objects if selection_only else bpy.data.objects
for obj in obj_iter:
items = list(obj.vertex_groups)
if filter_index:
if idx < len(items):
renaming_list.append(items[idx])
else:
renaming_list.extend(items)
elif context.scene.renaming_object_types == 'PARTICLESYSTEM':
if selection_only:
@@ -162,27 +190,64 @@ def get_renaming_list(context):
elif context.scene.renaming_object_types == 'UVMAPS':
filter_index = scene.renaming_filter_by_index
active_only = scene.renaming_active_only
idx = scene.renaming_index_target
for obj in obj_list:
if obj.type != 'MESH':
continue
for uv in obj.data.uv_layers:
renaming_list.append(uv)
if filter_index:
items = list(obj.data.uv_layers)
if idx < len(items):
item = items[idx]
if not active_only or obj.data.uv_layers.active == item:
renaming_list.append(item)
elif active_only:
active = obj.data.uv_layers.active
if active is not None:
renaming_list.append(active)
else:
for uv in obj.data.uv_layers:
renaming_list.append(uv)
elif context.scene.renaming_object_types == 'COLORATTRIBUTES':
filter_index = scene.renaming_filter_by_index
active_only = scene.renaming_active_only
idx = scene.renaming_index_target
for obj in obj_list:
if obj.type != 'MESH':
continue
for color_attribute in obj.data.color_attributes:
renaming_list.append(color_attribute)
if filter_index:
items = list(obj.data.color_attributes)
if idx < len(items):
item = items[idx]
if not active_only or obj.data.color_attributes.active_color == item:
renaming_list.append(item)
elif active_only:
active = obj.data.color_attributes.active_color
if active is not None:
renaming_list.append(active)
else:
for color_attribute in obj.data.color_attributes:
renaming_list.append(color_attribute)
elif context.scene.renaming_object_types == 'ATTRIBUTES':
filter_index = scene.renaming_filter_by_index
idx = scene.renaming_index_target
for obj in obj_list:
if obj.type != 'MESH':
continue
for attribute in obj.data.attributes:
renaming_list.append(attribute)
items = list(obj.data.attributes)
if filter_index:
if idx < len(items):
renaming_list.append(items[idx])
else:
renaming_list.extend(items)
elif scene.renaming_object_types == 'NODE_GROUPS':
renaming_list = list(bpy.data.node_groups)
elif scene.renaming_object_types == 'ACTIONS':
if selection_only:
@@ -263,6 +328,46 @@ def get_sorted_objects_z(objects):
return sorted_objects
def rename_data_if_enabled(scene, entity):
if scene.renaming_also_rename_data and \
scene.renaming_object_types in ('OBJECT', 'ADDOBJECTS'):
if hasattr(entity, 'data') and entity.data is not None:
entity.data.name = entity.name
def update_bone_drivers(old_name, new_name):
"""Update all driver paths that reference a renamed bone."""
if old_name == new_name:
return
# Blender may use either double or single quotes in data_path strings.
old_tokens = (f'pose.bones["{old_name}"]', f"pose.bones['{old_name}']")
new_token = f'pose.bones["{new_name}"]'
for datablock in list(bpy.data.objects) + list(bpy.data.scenes):
anim_data = getattr(datablock, 'animation_data', None)
if anim_data is None:
continue
for fcurve in anim_data.drivers:
# Location 1: FCurve data_path
for old_token in old_tokens:
if old_token in fcurve.data_path:
fcurve.data_path = fcurve.data_path.replace(old_token, new_token)
driver = fcurve.driver
if driver is None:
continue
for var in driver.variables:
for target in var.targets:
# Location 2: bone_target field
if target.bone_target == old_name:
target.bone_target = new_name
# Location 3: data_path inside variable target
for old_token in old_tokens:
if old_token in target.data_path:
target.data_path = target.data_path.replace(old_token, new_token)
def clear_order_flag(obj):
try:
del obj["selection_order"]
@@ -271,7 +376,7 @@ def clear_order_flag(obj):
def update_selection_order():
if not bpy.context.selected_objects:
if not (getattr(bpy.context, 'selected_objects', None) or list(bpy.context.view_layer.objects.selected)):
for o in bpy.data.objects:
clear_order_flag(o)
return
@@ -1,10 +1,102 @@
import re
import time
import bpy
from .renaming_operators import switch_to_edit_mode
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
from ..variable_replacer.variable_replacer import VariableReplacer
from .case_transform import to_upper, to_lower, upper_first, lower_first
# ---------------------------------------------------------------------------
# Regex replace with \u \l \U \L case modifier support
# Modifiers apply to the immediately following group reference ($N or \N).
# \u$1 — uppercase first char of group 1
# \l$1 — lowercase first char of group 1
# \U$1 — uppercase all of group 1
# \L$1 — lowercase all of group 1
# Both $1 and \1 are accepted as group references.
# ---------------------------------------------------------------------------
def _read_group_ref(repl, i, match):
"""Read a $N or \\N group reference at position i.
Returns (group_value, chars_consumed)."""
if i >= len(repl):
return '', 0
c = repl[i]
if c in ('$', '\\') and i + 1 < len(repl) and repl[i + 1].isdigit():
group_num = int(repl[i + 1])
try:
return match.group(group_num) or '', 2
except IndexError:
return '', 0
return '', 0
def _expand_replacement(repl, match):
"""Expand a replacement string, handling case modifiers and group refs."""
result = []
i = 0
n = len(repl)
while i < n:
c = repl[i]
if c == '\\' and i + 1 < n:
next_c = repl[i + 1]
if next_c in ('u', 'l', 'U', 'L'):
modifier = next_c
i += 2
group_val, advance = _read_group_ref(repl, i, match)
i += advance
if modifier == 'u':
group_val = upper_first(group_val)
elif modifier == 'l':
group_val = lower_first(group_val)
elif modifier == 'U':
group_val = to_upper(group_val)
elif modifier == 'L':
group_val = to_lower(group_val)
result.append(group_val)
elif next_c.isdigit():
group_num = int(next_c)
try:
result.append(match.group(group_num) or '')
except IndexError:
result.append('\\' + next_c)
i += 2
else:
result.append(c)
i += 1
elif c == '$' and i + 1 < n and repl[i + 1].isdigit():
group_num = int(repl[i + 1])
try:
result.append(match.group(group_num) or '')
except IndexError:
result.append(c)
i += 2
else:
result.append(c)
i += 1
return ''.join(result)
def regex_case_sub(pattern, repl, string):
"""re.sub that additionally supports \\u \\l \\U \\L case modifiers."""
if not re.search(r'\\[uUlL]', repl):
return re.sub(pattern, repl, string)
def replacer(match):
return _expand_replacement(repl, match)
return re.sub(pattern, replacer, string)
class VIEW3D_OT_search_and_replace(bpy.types.Operator):
@@ -25,11 +117,20 @@ class VIEW3D_OT_search_and_replace(bpy.types.Operator):
call_error_popup(context)
return {'CANCELLED'}
t_start = time.perf_counter()
searchName = wm.renaming_search
msg = wm.renaming_messages # variable to save messages
VariableReplacer.reset()
VariableReplacer.prepare(context)
# When the search string contains no @ variables it is the same for
# every entity, so the case-insensitive pattern can be compiled once.
search_has_variables = '@' in searchName
static_pattern = None
if not wm.renaming_useRegex and not wm.renaming_matchcase and not search_has_variables and searchName != '':
static_pattern = re.compile(re.escape(searchName), re.IGNORECASE)
if len(renaming_list) > 0:
for entity in renaming_list: # iterate over all objects that are to be renamed
@@ -41,19 +142,18 @@ class VIEW3D_OT_search_and_replace(bpy.types.Operator):
if not wm.renaming_useRegex:
if wm.renaming_matchcase:
new_name = str(entity.name).replace(searchReplaced, replaceReplaced)
entity.name = new_name
msg.add_message(oldName, entity.name)
else:
replaceSearch = re.compile(re.escape(searchReplaced), re.IGNORECASE)
new_name = replaceSearch.sub(replaceReplaced, entity.name)
entity.name = new_name
msg.add_message(oldName, entity.name)
pattern = static_pattern or re.compile(re.escape(searchReplaced), re.IGNORECASE)
new_name = pattern.sub(replaceReplaced, entity.name)
else: # Use regex
# pattern = re.compile(re.escape(searchName))
new_name = re.sub(searchReplaced, replaceReplaced, str(entity.name))
entity.name = new_name
msg.add_message(oldName, entity.name)
new_name = regex_case_sub(searchReplaced, replaceReplaced, str(entity.name))
entity.name = new_name
rename_data_if_enabled(wm, entity)
if wm.renaming_object_types == 'BONE':
update_bone_drivers(oldName, entity.name)
msg.add_message(oldName, entity.name)
log_timing(context, "search_replace", t_start, len(renaming_list))
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
@@ -29,6 +29,7 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
def execute(self, context):
super().execute(context)
VariableReplacer.prepare(context)
wm = context.scene
# get list of objects to be selected
@@ -56,8 +57,15 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
selectionList.append(entity)
msg.add_message("selected", entityName)
else:
if re.search(searchReplaced, entityName, re.IGNORECASE):
selectionList.append(entity)
try:
if re.search(searchReplaced, entityName, re.IGNORECASE):
selectionList.append(entity)
except re.error as err:
# invalid regex, add message but continue so other names can still be processed
error_msg = f"Invalid regular expression in search: {err}"
wm.renaming_error_messages.add_message(error_msg)
call_error_popup(context)
return {'CANCELLED'}
if str(wm.renaming_object_types) == 'OBJECT':
# set to object mode
@@ -74,7 +82,7 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
if bpy.context.mode == 'POSE':
bpy.ops.pose.select_all(action='DESELECT')
for bone in selectionList:
bone.select = True
bone.bone.select = True
elif bpy.context.mode == 'EDIT_ARMATURE':
bpy.ops.armature.select_all(action='DESELECT')
@@ -1,8 +1,11 @@
import time
import bpy
from .renaming_operators import switch_to_edit_mode
from ..operators.renaming_utilities import get_renaming_list, trim_string, call_renaming_popup, call_error_popup
from ..operators.renaming_utilities import get_renaming_list, trim_string, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
class VIEW3D_OT_trim_string(bpy.types.Operator):
bl_idname = "renaming.trim_string"
bl_label = "Trim String"
@@ -19,6 +22,7 @@ class VIEW3D_OT_trim_string(bpy.types.Operator):
call_error_popup(context)
return {'CANCELLED'}
t_start = time.perf_counter()
msg = wm.renaming_messages
if len(renaming_list) > 0:
@@ -27,11 +31,15 @@ class VIEW3D_OT_trim_string(bpy.types.Operator):
old_name = entity.name
new_name = trim_string(entity.name, wm.renaming_trim_indices)
entity.name = new_name
rename_data_if_enabled(wm, entity)
if wm.renaming_object_types == 'BONE':
update_bone_drivers(old_name, entity.name)
msg.add_message(old_name, entity.name)
log_timing(context, "trim_string", t_start, len(renaming_list))
call_renaming_popup(context)
if switch_edit_mode:
switch_to_edit_mode(context)
return {'FINISHED'}
@@ -0,0 +1,62 @@
import threading
import urllib.request
import urllib.error
import json
# Module-level state — read by the panel draw function
update_available = False
latest_version_str = ""
_RELEASES_URL = "https://api.github.com/repos/Weisl/simple_renaming/releases/latest"
def _parse_version(version_str):
"""Convert '2.1.4' or 'v2.1.4' to (2, 1, 4)."""
return tuple(int(x) for x in version_str.lstrip("v").split("."))
def _fetch():
global update_available, latest_version_str
try:
req = urllib.request.Request(
_RELEASES_URL,
headers={"User-Agent": "simple-renaming-addon"},
)
with urllib.request.urlopen(req, timeout=5) as response:
data = json.loads(response.read().decode())
tag = data.get("tag_name", "")
if not tag:
return
latest = _parse_version(tag)
# Read current version from blender_manifest.toml at the addon root
import os
manifest_path = os.path.join(os.path.dirname(__file__), "..", "blender_manifest.toml")
current_str = ""
with open(manifest_path, encoding="utf-8") as f:
for line in f:
if line.startswith("version"):
current_str = line.split("=")[1].strip().strip('"')
break
if not current_str:
return
current = _parse_version(current_str)
if latest > current:
update_available = True
latest_version_str = tag.lstrip("v")
else:
print(f"[RENAMING] Addon is up to date (v{current_str})")
except Exception as exc:
print(f"[RENAMING] version check failed: {exc}")
def start_version_check():
"""Fire a background thread to check for a newer release on GitHub."""
t = threading.Thread(target=_fetch, daemon=True)
t.start()
@@ -26,10 +26,6 @@ class BUTTON_OT_change_key(bpy.types.Operator):
property_prefix: bpy.props.StringProperty()
def __init__(self):
self.prefs = None
self.my_event = ''
def invoke(self, context, event):
prefs = context.preferences.addons[base_package].preferences
self.prefs = prefs
@@ -57,7 +53,7 @@ class BUTTON_OT_change_key(bpy.types.Operator):
def add_keymap():
km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name="Window")
km = bpy.context.window_manager.keyconfigs.active.keymaps.new(name="Window")
prefs = bpy.context.preferences.addons[base_package].preferences
kmi = km.keymap_items.new(idname='wm.call_panel', type=prefs.renaming_panel_type, value='PRESS',
@@ -80,7 +76,7 @@ def add_key_to_keymap(idname, kmi, km, active=True):
def remove_key(context, idname, properties_name):
"""Removes addon hotkeys from the keymap"""
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps['Window']
km = wm.keyconfigs.active.keymaps['Window']
for kmi in km.keymap_items:
if kmi.idname == idname and kmi.properties.name == properties_name:
@@ -91,7 +87,7 @@ def remove_keymap():
"""Removes keys from the keymap. Currently, this is only called when unregistering the addon. """
# only works for menus and pie menus
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps['Window']
km = wm.keyconfigs.active.keymaps['Window']
for kmi in km.keymap_items:
if hasattr(kmi.properties, 'name') and kmi.properties.name in ['VIEW3D_PT_tools_renaming_panel',
@@ -7,7 +7,6 @@ from bpy.props import (
from .renaming_keymap import remove_key
from .. import __package__ as base_package
from ..ui.renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix
def label_multiline(context, text, parent):
@@ -29,7 +28,7 @@ def add_key(km, idname, properties_name, button_assignment_type, button_assignme
def update_key(context, operation, operator_name, property_prefix):
# This functions gets called when the hotkey assignment is updated in the preferences
wm = context.window_manager
km = wm.keyconfigs.addon.keymaps["Window"]
km = wm.keyconfigs.active.keymaps["Window"]
prefs = context.preferences.addons[base_package].preferences
@@ -51,6 +50,7 @@ def update_suf_pre_key(self, context):
def update_panel_category(self, context):
"""Update panel tab for collider tools"""
from ..ui.renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix
panels = [
VIEW3D_PT_tools_renaming_panel,
@@ -70,6 +70,7 @@ def update_panel_category(self, context):
def toggle_suffix_prefix_panel(self, context):
from ..ui.renaming_panels import VIEW3D_PT_tools_type_suffix
if self.renaming_show_suffix_prefix_panel:
bpy.utils.register_class(VIEW3D_PT_tools_type_suffix)
else:
@@ -107,6 +108,12 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
default=True,
)
debug_timing: bpy.props.BoolProperty(
name="Debug Timing",
description="Print operator execution time to the console after each rename operation",
default=False,
)
renamingPanel_useObjectOrder: bpy.props.BoolProperty(
name="Use Selection Order",
description="Use the order of selection when renaming objects",
@@ -162,6 +169,25 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
default='',
)
date_format: StringProperty(
name="Date Format",
description=(
"strftime format string for the @d variable. "
"Codes: %d=day(03), %m=month(04), %y=year(26), %Y=year(2026), %b=month abbr(Apr). "
"Examples: %d%m%Y → 03042026 (DDMMYYYY), %m%d%y → 040326 (MMDDYY), %d%b%Y → 03Apr2026"
),
default="%y%m%d",
)
time_format: StringProperty(
name="Time Format",
description=(
"strftime format string for the @i variable. "
"Codes: %H=hour 24h(14), %M=minute(30), %S=second(05), %I=hour 12h(02), %p=AM/PM. "
"Example: %H%M → 1430. Avoid colons — invalid in filenames on Windows"
),
default="%H%M",
)
renaming_show_suffix_prefix_panel: bpy.props.BoolProperty(
name="Prefix/Suffix by Type Panel",
description="Enable or disable the Prefix/Suffix by Type Panel",
@@ -185,7 +211,7 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
"renamingPanel_showPopup",
"renaming_show_suffix_prefix_panel",
"renamingPanel_useObjectOrder",
"debug_timing",
]
props_naming = [
"renaming_separator",
@@ -205,6 +231,11 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
"renaming_user3"
]
props_date_time = [
"date_format",
"time_format",
]
renaming_panel_type: bpy.props.StringProperty(
name="Renaming Popup",
default="F2",
@@ -325,6 +356,13 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
row = box.row()
row.prop(self, propName)
box = layout.box()
row = box.row()
row.label(text='Date & Time Variables')
for propName in self.props_date_time:
row = box.row()
row.prop(self, propName)
box = layout.box()
row = box.row()
row.label(text='User Variables')
@@ -2,7 +2,7 @@ import bpy
from .info_messages import RENAMING_MESSAGES, WarningError_MESSAGES, INFO_MESSAGES
from .renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix, VIEW3D_OT_SetVariable, \
VIEW3D_OT_RenamingPopupOperator, OBJECT_MT_suffix_prefix_presets, AddPresetRenamingPresets
VIEW3D_OT_RenamingPopupOperator, OBJECT_MT_suffix_prefix_presets, AddPresetRenamingPresets, RENAMING_MT_caseMenu
from .renaming_panels import panel_func
from .renaming_popup import VIEW3D_PT_renaming_popup, VIEW3D_PT_info_popup, VIEW3D_PT_error_popup
from .renaming_variables import RENAMING_MT_variableMenu, VIEW3D_OT_inputVariables
@@ -10,6 +10,7 @@ from .ui_helpers import PREFERENCES_OT_open_addon
classes = (
RENAMING_MT_variableMenu,
RENAMING_MT_caseMenu,
VIEW3D_OT_inputVariables,
VIEW3D_PT_error_popup,
VIEW3D_PT_info_popup,
@@ -42,6 +43,8 @@ def register():
def unregister():
from bpy.utils import unregister_class
VIEW3D_PT_tools_type_suffix.remove(panel_func)
for cls in reversed(classes):
unregister_class(cls)
@@ -15,6 +15,13 @@ types_of_selected = (
def draw_renaming_panel(layout, context):
from ..operators.version_check import update_available, latest_version_str
if update_available:
row = layout.row(align=True)
row.alert = True
row.label(text=f"Update available: v{latest_version_str}", icon='ERROR')
scene = context.scene
row = layout.row(align=True)
@@ -25,8 +32,18 @@ def draw_renaming_panel(layout, context):
# SELECTED
if str(scene.renaming_object_types) == 'OBJECT':
layout.prop(scene, "renaming_object_types_specified", expand=True)
layout.prop(scene, "renaming_also_rename_data")
if str(scene.renaming_object_types) in types_of_selected:
layout.prop(scene, "renaming_only_selection", text="Only Of Selected Objects")
if str(scene.renaming_object_types) in ('UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'VERTEXGROUPS', 'SHAPEKEYS'):
col = layout.column(align=True)
if str(scene.renaming_object_types) in ('UVMAPS', 'COLORATTRIBUTES'):
col.prop(scene, "renaming_active_only")
row = col.row(align=True)
row.prop(scene, "renaming_filter_by_index")
sub = row.row(align=True)
sub.enabled = scene.renaming_filter_by_index
sub.prop(scene, "renaming_index_target", text="")
elif str(scene.renaming_object_types) in types_selected:
layout.prop(scene, "renaming_only_selection", text="Only Selected")
elif str(scene.renaming_object_types) == 'COLLECTION':
@@ -44,7 +61,7 @@ def draw_renaming_panel(layout, context):
box = layout
# Sorting
if str(scene.renaming_object_types) not in ['COLLECTION', 'IMAGE']:
if str(scene.renaming_object_types) not in ['COLLECTION', 'IMAGE', 'NODE_GROUPS']:
col = box.column(align=True)
col.prop(scene, "renaming_sorting")
if scene.renaming_sorting:
@@ -140,7 +157,9 @@ def draw_renaming_panel(layout, context):
layout.label(text="Other")
row = layout.row(align=True)
row.operator("renaming.numerate", icon="LINENUMBERS_ON")
row.operator("renaming.numerate", icon="LINENUMBERS_ON")
row = layout.row(align=True)
row.menu("RENAMING_MT_case_menu", text="Case Transform")
if str(scene.renaming_object_types) in ('DATA', 'OBJECT', 'ADDOBJECTS'):
layout.separator()
@@ -178,6 +197,7 @@ class VIEW3D_PT_tools_renaming_panel(bpy.types.Panel):
op = row.operator("preferences.rename_addon_search", text="", icon='PREFERENCES')
op.addon_name = addon_name
op.prefs_tabs = 'UI'
row.operator("renaming.reload_addon", text="", icon='FILE_REFRESH')
def draw(self, context):
layout = self.layout
@@ -282,10 +302,30 @@ class VIEW3D_PT_tools_type_suffix(bpy.types.Panel):
row.prop(scene, "renaming_suffix_prefix_lightprops", text="")
row.operator('renaming.add_suffix_prefix_by_type', text="Light Probes").option = 'lightprops'
row = col.row()
row.prop(scene, "renaming_suffix_prefix_pointcloud", text="")
row.operator('renaming.add_suffix_prefix_by_type', text="Point Clouds").option = 'pointcloud'
row = col.row()
row.operator('renaming.add_suffix_prefix_by_type', text="Rename All").option = 'all'
class RENAMING_MT_caseMenu(bpy.types.Menu):
bl_label = "Case"
bl_idname = "RENAMING_MT_case_menu"
def draw(self, context):
layout = self.layout
layout.operator("renaming.case_upper", text="UPPERCASE")
layout.operator("renaming.case_lower", text="lowercase")
layout.separator()
layout.operator("renaming.case_pascal", text="PascalCase")
layout.operator("renaming.case_camel", text="camelCase")
layout.separator()
layout.operator("renaming.case_snake", text="snake_case")
layout.operator("renaming.case_kebab", text="kebab-case")
class VIEW3D_OT_SetVariable(bpy.types.Operator):
"""Tooltip"""
bl_idname = "object.renaming_set_variable"
@@ -353,6 +393,7 @@ class AddPresetRenamingPresets(AddPresetBase, Operator):
"scene.renaming_suffix_prefix_bone",
"scene.renaming_suffix_prefix_speakers",
"scene.renaming_suffix_prefix_lightprops",
"scene.renaming_suffix_prefix_pointcloud",
]
# where to store the preset
@@ -31,10 +31,26 @@ class RENAMING_MT_variableMenu(bpy.types.Menu):
layout.operator("object.renaming_multivariables", text="PARENT").renaming_variables = "PARENT"
layout.operator("object.renaming_multivariables", text="DATA").renaming_variables = "DATA"
layout.operator("object.renaming_multivariables", text="ACTIVE").renaming_variables = "ACTIVE"
layout.operator("object.renaming_multivariables", text='FILE').renaming_variables = 'OBJECT'
layout.operator("object.renaming_multivariables", text='OBJECT').renaming_variables = 'OBJECT'
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
layout.operator("object.renaming_multivariables", text="COLLECTION").renaming_variables = "COLLECTION"
if wm.renaming_object_types == 'NODE_GROUPS':
layout.separator()
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
if wm.renaming_object_types in (
'UVMAPS', 'MATERIAL', 'BONE', 'MODIFIERS', 'SHAPEKEYS',
'VERTEXGROUPS', 'PARTICLESYSTEM', 'COLORATTRIBUTES', 'ATTRIBUTES',
):
layout.separator()
layout.operator("object.renaming_multivariables", text="OBJECT").renaming_variables = "OBJECT"
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
layout.operator("object.renaming_multivariables", text="PARENT").renaming_variables = "PARENT"
layout.operator("object.renaming_multivariables", text="DATA").renaming_variables = "DATA"
layout.operator("object.renaming_multivariables", text="ACTIVE").renaming_variables = "ACTIVE"
layout.operator("object.renaming_multivariables", text="COLLECTION").renaming_variables = "COLLECTION"
class VIEW3D_OT_inputVariables(bpy.types.Operator):
"""Tooltip"""
@@ -22,7 +22,7 @@ class PREFERENCES_OT_open_addon(bpy.types.Operator):
prefs.prefs_tabs = self.prefs_tabs
import addon_utils
mod = addon_utils.addons_fake_modules.get('collider_tools')
mod = addon_utils.addons_fake_modules.get('simple_renaming')
# mod is None the first time the operation is called :/
if mod:
@@ -7,6 +7,11 @@ import bpy
from .. import __package__ as base_package
# Single compiled pattern covering all supported variables.
# Multi-char tokens (@u1/@u2/@u3) are listed before the single-char fallback
# so the alternation matches them first.
_VARIABLE_RE = re.compile(r'@(?:u[123]|[fdirhlobantpmc])')
def generate_random_string(string_length=10):
"""Generate a random string of fixed length """
@@ -23,6 +28,12 @@ class VariableReplacer:
step = 1
start_number = 0
# Per-operation lookup caches built by prepare()
_collection_cache = {} # obj_name -> concatenated collection names
_material_to_obj = {} # material_name -> first owner object name
_shape_key_to_obj = {} # id(Key datablock) -> owner object name
_mesh_arm_to_obj = {} # id(obj.data) -> owner object name
@classmethod
def reset(cls):
"""reset all values to initial state"""
@@ -38,44 +49,126 @@ class VariableReplacer:
cls.number = 0
@classmethod
def replaceInputString(cls, context, inputText, entity):
def prepare(cls, context):
"""Build per-operation lookup caches before the rename loop.
Call this once per operator execution after reset(). The caches turn
O(collections × objects) and O(objects) per-entity lookups into O(1).
"""
# Collection reverse-lookup: obj_name -> concatenated collection names
collection_cache = {}
for col in bpy.data.collections:
for obj in col.objects:
if obj.name in collection_cache:
collection_cache[obj.name] += col.name
else:
collection_cache[obj.name] = col.name
cls._collection_cache = collection_cache
# Material -> first owner object name
material_to_obj = {}
for obj in bpy.data.objects:
for slot in obj.material_slots:
if slot.material and slot.material.name not in material_to_obj:
material_to_obj[slot.material.name] = obj.name
cls._material_to_obj = material_to_obj
# Shape key (Key datablock) -> owner object name
shape_key_to_obj = {}
mesh_arm_to_obj = {}
for obj in bpy.data.objects:
if obj.data is None:
continue
data_id = id(obj.data)
if data_id not in mesh_arm_to_obj:
mesh_arm_to_obj[data_id] = obj.name
if hasattr(obj.data, 'shape_keys') and obj.data.shape_keys is not None:
sk_id = id(obj.data.shape_keys)
if sk_id not in shape_key_to_obj:
shape_key_to_obj[sk_id] = obj.name
cls._shape_key_to_obj = shape_key_to_obj
cls._mesh_arm_to_obj = mesh_arm_to_obj
@classmethod
def replaceInputString(cls, context, inputText, entity):
"""Replace custom variables with the according string"""
wm = context.scene
cls.addon_prefs = context.preferences.addons[base_package].preferences
# System and Global Values #
inputText = re.sub(r'@f', cls.getfileName(context), inputText) # file name
inputText = re.sub(r'@d', cls.getDateName(), inputText) # date
inputText = re.sub(r'@i', cls.getTimeName(), inputText) # time
inputText = re.sub(r'@r', cls.getRandomString(), inputText)
if '@' not in inputText:
return inputText
# UserStrings #
inputText = re.sub(r'@h', cls.get_high_variable(), inputText) # high
inputText = re.sub(r'@l', cls.get_low_variable(), inputText) # low
inputText = re.sub(r'@b', cls.get_cage_variable(), inputText) # cage
inputText = re.sub(r'@u1', cls.getuser1(), inputText)
inputText = re.sub(r'@u2', cls.getuser2(), inputText)
inputText = re.sub(r'@u3', cls.getuser3(), inputText)
# Find only the variables present in this template so we skip calling
# getters that are not needed (lazy evaluation).
vars_present = set(_VARIABLE_RE.findall(inputText))
# GetScene #
inputText = re.sub(r'@a', cls.getActive(context), inputText) # active object
inputText = re.sub(r'@n', cls.getNumber(), inputText)
replacements = {}
if '@n' in vars_present:
replacements['@n'] = cls.getNumber()
if '@f' in vars_present:
replacements['@f'] = cls.getfileName(context)
if '@d' in vars_present:
replacements['@d'] = cls.getDateName()
if '@i' in vars_present:
replacements['@i'] = cls.getTimeName()
if '@r' in vars_present:
replacements['@r'] = cls.getRandomString()
if '@h' in vars_present:
replacements['@h'] = cls.get_high_variable()
if '@l' in vars_present:
replacements['@l'] = cls.get_low_variable()
if '@b' in vars_present:
replacements['@b'] = cls.get_cage_variable()
if '@u1' in vars_present:
replacements['@u1'] = cls.getuser1()
if '@u2' in vars_present:
replacements['@u2'] = cls.getuser2()
if '@u3' in vars_present:
replacements['@u3'] = cls.getuser3()
if '@a' in vars_present:
replacements['@a'] = cls.getActive(context)
if wm.renaming_object_types == 'OBJECT':
# Objects
inputText = re.sub(r'@o', cls.getObject(entity), inputText) # object
inputText = re.sub(r'@t', cls.getType(entity), inputText) # type
inputText = re.sub(r'@p', cls.getParent(entity), inputText) # parent
inputText = re.sub(r'@m', cls.getData(entity), inputText) # data
inputText = re.sub(r'@c', cls.getCollection(entity), inputText) # collection
if '@o' in vars_present:
replacements['@o'] = cls.getObject(entity)
if '@t' in vars_present:
replacements['@t'] = cls.getType(entity)
if '@p' in vars_present:
replacements['@p'] = cls.getParent(entity)
if '@m' in vars_present:
replacements['@m'] = cls.getData(entity)
if '@c' in vars_present:
replacements['@c'] = cls.getCollection(entity)
if wm.renaming_object_types in (
'UVMAPS', 'MATERIAL', 'BONE', 'MODIFIERS', 'SHAPEKEYS',
'VERTEXGROUPS', 'PARTICLESYSTEM', 'COLORATTRIBUTES', 'ATTRIBUTES',
):
owner_obj = bpy.data.objects.get(cls.getOwnerObjectName(entity))
if owner_obj is not None:
if '@o' in vars_present:
replacements['@o'] = owner_obj.name
if '@t' in vars_present:
replacements['@t'] = cls.getType(owner_obj)
if '@p' in vars_present:
replacements['@p'] = cls.getParent(owner_obj)
if '@m' in vars_present:
replacements['@m'] = cls.getData(owner_obj)
if '@c' in vars_present:
replacements['@c'] = cls.getCollection(owner_obj)
if wm.renaming_object_types == 'NODE_GROUPS':
if '@t' in vars_present:
replacements['@t'] = cls.getType(entity)
# IMAGES #
if wm.renaming_object_types == 'IMAGE':
inputText = re.sub(r'@r', 'RESOLUTION', inputText)
inputText = re.sub(r'@i', 'FILETYPE', inputText)
if '@r' in vars_present:
replacements['@r'] = 'RESOLUTION'
if '@i' in vars_present:
replacements['@i'] = 'FILETYPE'
return inputText
return _VARIABLE_RE.sub(lambda m: replacements.get(m.group(), m.group()), inputText)
@staticmethod
def getRandomString():
@@ -125,26 +218,24 @@ class VariableReplacer:
@classmethod
def getfileName(cls, context):
scn = context.scene
if bpy.data.is_saved:
filename = bpy.path.display_name(context.blend_data.filepath)
else:
filename = "UNSAVED"
# scn.renaming_messages.add_message(oldName, entity.name)
context.scene.renaming_error_messages.add_message(
"@f variable: file is unsaved, replaced with 'UNSAVED'", isError=False
)
return filename
@classmethod
def getDateName(cls):
t = time.localtime()
t = time.mktime(t)
return time.strftime("%d%b%Y", time.gmtime(t))
date_format = cls.addon_prefs.date_format if cls.addon_prefs else "%d%b%Y"
return time.strftime(date_format, time.localtime())
@classmethod
def getTimeName(cls):
t = time.localtime()
t = time.mktime(t)
return time.strftime("%H:%M", time.gmtime(t))
time_format = cls.addon_prefs.time_format if cls.addon_prefs else "%H%M"
return time.strftime(time_format, time.localtime())
@classmethod
def getActive(cls, context):
@@ -161,29 +252,60 @@ class VariableReplacer:
@classmethod
def getType(cls, entity):
return str(entity.type)
if entity is None:
return "NO_TYPE"
try:
return str(entity.type)
except AttributeError:
return "NO_TYPE"
@classmethod
def getParent(cls, entity):
if entity.parent is not None:
return str(entity.parent.name)
else:
return entity.name
if entity is None:
return "NO_PARENT"
try:
if entity.parent is not None:
return str(entity.parent.name)
else:
return entity.name
except AttributeError:
return "NO_PARENT"
@classmethod
def getData(cls, entity):
if entity.data is not None:
return str(entity.data.name)
else:
return entity.name
if entity is None:
return "NO_DATA"
try:
if entity.data is not None:
return str(entity.data.name)
else:
return entity.name
except AttributeError:
return "NO_DATA"
@classmethod
def getCollection(cls, entity):
"""O(1) lookup using cache built by prepare()."""
return cls._collection_cache.get(entity.name, "")
collectionew_names = ""
for collection in bpy.data.collections:
collection_objects = collection.objects
if entity.name in collection.objects and entity in collection_objects[:]:
collectionew_names += collection.name
@classmethod
def getOwnerObjectName(cls, entity):
"""Find the owner object name using caches built by prepare()."""
id_data = getattr(entity, 'id_data', None)
if id_data is None:
return ""
return collectionew_names
# Modifier, vertex group, particle system, pose bone — id_data is the Object directly
if id_data.bl_rna.identifier == 'Object':
return id_data.name
# Shape key — id_data is a Key datablock
if id_data.bl_rna.identifier == 'Key':
return cls._shape_key_to_obj.get(id(id_data), "")
# Material — search by material name
if id_data.bl_rna.identifier == 'Material':
return cls._material_to_obj.get(id_data.name, "")
# UV layer, bone — id_data is a Mesh or Armature datablock
return cls._mesh_arm_to_obj.get(id(id_data), "")