177 lines
6.6 KiB
Python
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()
|