Utility_Apps/Blender/addons/Edit Collection Instance in Place/__init__.py

177 lines
6.6 KiB
Python

bl_info = {
"name": "Edit Collection Instance In-Place (Nested & Non-Destructive)",
"author": "ChatGPT + Ryan Schultz",
"version": (1, 5),
"blender": (3, 0, 0),
"location": "View3D > Sidebar > Edit Instance",
"description": "Recursively edits collection instances in place, supporting nesting and new objects.",
"category": "Object",
}
import bpy
from mathutils import Matrix
SESSION_STACK = [] # Stack of editing sessions
def get_all_objects_recursive(coll, seen=None):
if seen is None:
seen = set()
objs = list(coll.objects)
for child_coll in coll.children:
if child_coll.name not in seen:
seen.add(child_coll.name)
objs.extend(get_all_objects_recursive(child_coll, seen))
return objs
def print_collection_debug_info(coll, label="STATE"):
print(f"\n--- {label} Collection '{coll.name}' World Locations ---")
for o in get_all_objects_recursive(coll):
print(f"Object '{o.name}' - Location: {o.location}")
print("------------------------------------")
print(f"--- Instance Locations ---")
for inst in bpy.context.scene.objects:
if inst.type == 'EMPTY' and inst.instance_collection == coll:
print(f"Instance '{inst.name}' - Location: {inst.location}")
print("------------------------------------\n")
print(f">>> World Vertices of Collection '{coll.name}' (Direct Objects)")
for obj in get_all_objects_recursive(coll):
if obj.type == 'MESH' and obj.data:
print(f"Object '{obj.name}' has {len(obj.data.vertices)} vertices:")
for i, v in enumerate(obj.data.vertices):
world_vert = obj.matrix_world @ v.co
print(f" Vert {i}: {world_vert}")
print("------------------------------------------------")
def print_instance_vertex_info(coll):
print(f"\n>>> World Vertices for Instances of Collection '{coll.name}'")
for inst in bpy.context.scene.objects:
if inst.type == 'EMPTY' and inst.instance_collection == coll:
print(f"Instance '{inst.name}' at {inst.location}:")
for obj in get_all_objects_recursive(coll):
if obj.type == 'MESH':
print(f" Object '{obj.name}' ({len(obj.data.vertices)} vertices):")
for i, v in enumerate(obj.data.vertices):
world_vert = inst.matrix_world @ (obj.matrix_basis @ v.co)
print(f" Vert {i}: {world_vert}")
print("------------------------------------------------")
class OBJECT_OT_StartEditInstanceSmart(bpy.types.Operator):
bl_idname = "object.start_edit_instance_smart"
bl_label = "Start Edit In-Place"
bl_description = "Begin editing this collection instance in-place (nested supported)"
def execute(self, context):
obj = context.active_object
if not obj or obj.instance_type != 'COLLECTION' or not obj.instance_collection:
self.report({'ERROR'}, "Select a collection instance (Empty)")
return {'CANCELLED'}
coll = obj.instance_collection
session = {
'collection': coll,
'original_location': {},
'hidden_instances': [],
'original_names': set(),
'instance_matrix_world': obj.matrix_world.copy(),
'instance_inv_matrix': obj.matrix_world.inverted_safe(),
}
print_collection_debug_info(coll, label="Before Edit")
print_instance_vertex_info(coll)
for o in get_all_objects_recursive(coll):
session['original_location'][o.name] = o.matrix_world.copy()
session['original_names'].add(o.name)
o.matrix_world = obj.matrix_world @ o.matrix_basis
for o in context.scene.objects:
if o.type == 'EMPTY' and o != obj and o.instance_collection == coll:
o.hide_viewport = True
o.hide_render = True
session['hidden_instances'].append(o)
obj.hide_viewport = True
obj.hide_render = True
session['hidden_instances'].append(obj)
SESSION_STACK.append(session)
self.report({'INFO'}, f"Editing collection '{coll.name}' in-place. Nesting level: {len(SESSION_STACK)}")
return {'FINISHED'}
class OBJECT_OT_FinishEditInstanceSmart(bpy.types.Operator):
bl_idname = "object.finish_edit_instance_smart"
bl_label = "Finish Edit In-Place"
bl_description = "Finish the current nested collection edit"
def execute(self, context):
if not SESSION_STACK:
self.report({'WARNING'}, "No active editing session")
return {'CANCELLED'}
session = SESSION_STACK.pop()
coll = session['collection']
original_names = session['original_names']
instance_inv = session['instance_inv_matrix']
print_collection_debug_info(coll, label="DURING (at finish)")
print_instance_vertex_info(coll)
for obj in get_all_objects_recursive(coll):
current_world = obj.matrix_world.copy()
local_matrix = instance_inv @ current_world
loc, rot, scale = local_matrix.decompose()
obj.location = loc
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = rot
obj.scale = scale
if obj.name not in original_names:
print(f"New object detected: {obj.name} (handled properly)")
for o in session['hidden_instances']:
o.hide_viewport = False
o.hide_render = False
print_collection_debug_info(coll, label="AFTER Edit")
print_instance_vertex_info(coll)
self.report({'INFO'}, f"Finished editing collection '{coll.name}'. Remaining nesting: {len(SESSION_STACK)}")
return {'FINISHED'}
class VIEW3D_PT_EditInstanceSmart(bpy.types.Panel):
bl_label = "Edit Instance (Nested)"
bl_idname = "VIEW3D_PT_edit_instance_smart"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit Instance'
def draw(self, context):
layout = self.layout
layout.operator("object.start_edit_instance_smart", icon='OUTLINER_COLLECTION')
layout.operator("object.finish_edit_instance_smart", icon='CHECKMARK')
layout.label(text=f"Nesting Level: {len(SESSION_STACK)}")
classes = (
OBJECT_OT_StartEditInstanceSmart,
OBJECT_OT_FinishEditInstanceSmart,
VIEW3D_PT_EditInstanceSmart,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
print("Nested Smart Edit Collection Instance plugin loaded.")
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()