work
save startup blend for animation tab & whatnot
This commit is contained in:
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='')
|
||||
|
||||
|
||||
+13
-3
@@ -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:
|
||||
|
||||
+172
-50
@@ -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), "")
|
||||
|
||||
Reference in New Issue
Block a user