Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """Convert selected objects to containing collection rows in View Layer Outliners.
- Run in Object Mode. Collection branches are expanded; filters are restored.
- Selection runs over several redraws. Do not interact with Outliners until done.
- """
- import bpy
- PREFIX = "[Collection Activator]"
- RUNNING_KEY = "collection_row_selection_running"
- def layer_collections(root):
- if root.exclude:
- return
- # The view-layer root is the Outliner "Scene Collection" row, not a
- # user collection and must never be part of the replacement selection.
- for child in root.children:
- if child.exclude:
- continue
- yield child, 1
- for layer, depth in layer_collections(child):
- yield layer, depth + 1
- def target_collections(objects, root):
- """All direct memberships in this View Layer, deduplicated."""
- available = {layer.collection for layer, _ in layer_collections(root)}
- return {c for obj in objects for c in obj.users_collection if c in available}
- def outliner_contexts(window):
- for area in window.screen.areas:
- if area.type != "OUTLINER" or area.spaces.active.display_mode != "VIEW_LAYER":
- continue
- region = next((r for r in area.regions if r.type == "WINDOW"), None)
- if region:
- yield {"window": window, "area": area, "region": region}
- def selected_ids():
- return set(bpy.context.selected_ids)
- def box_row(region, y, mode):
- result = bpy.ops.outliner.select_box(
- "EXEC_DEFAULT", xmin=0, xmax=max(1, region.width - 1),
- ymin=y, ymax=y, mode=mode,
- )
- if "FINISHED" not in result:
- raise RuntimeError("Outliner box selection cancelled")
- def select_rows(override, targets, depth):
- """Box-select rows by Y, retaining only verified target collection IDs."""
- area, region = override["area"], override["region"]
- space = area.spaces.active
- properties = ("filter_text", "use_filter_object", "use_filter_collection",
- "use_filter_view_layers", "use_sync_select")
- saved = {name: getattr(space, name) for name in properties}
- try:
- space.use_sync_select = False
- space.filter_text = ""
- space.use_filter_object = False
- space.use_filter_collection = True
- space.use_filter_view_layers = False
- area.tag_redraw()
- yield
- with bpy.context.temp_override(**override):
- for _ in range(depth + 2):
- bpy.ops.outliner.show_one_level(open=True)
- area.tag_redraw()
- yield
- with bpy.context.temp_override(**override):
- bpy.ops.outliner.select_all(action="DESELECT")
- previous = None
- for _ in range(10000):
- position = region.view2d.region_to_view(0, 0)[1]
- if previous is not None and abs(position - previous) < 0.01:
- break
- previous = position
- with bpy.context.temp_override(**override):
- bpy.ops.outliner.scroll_page(up=True)
- yield
- else:
- raise RuntimeError("Could not reach Outliner top")
- previous = None
- for _ in range(10000):
- position = region.view2d.region_to_view(0, 0)[1]
- if previous is not None and abs(position - previous) < 0.01:
- break
- previous = position
- with bpy.context.temp_override(**override):
- for y in range(2, region.height - 2, 4):
- # Probe this row independently, including already selected
- # rows. Some structural rows expose no selected ID at all.
- box_row(region, y, "SUB")
- before = selected_ids()
- box_row(region, y, "ADD")
- added = selected_ids() - before
- if not added or added - targets:
- box_row(region, y, "SUB")
- if selected_ids() == targets:
- break
- bpy.ops.outliner.scroll_page(up=False)
- yield
- else:
- raise RuntimeError("Outliner page limit exceeded")
- with bpy.context.temp_override(**override):
- actual = selected_ids()
- if actual != targets:
- missing = ", ".join(sorted(c.name for c in targets - actual))
- raise RuntimeError(f"Collection rows not found: {missing}")
- finally:
- for name, value in saved.items():
- setattr(space, name, value)
- area.tag_redraw()
- yield
- with bpy.context.temp_override(**override):
- if selected_ids() != targets:
- raise RuntimeError("Selection changed after restoring filters/redrawing")
- def main():
- print(f"{PREFIX} Script started.")
- if bpy.app.driver_namespace.get(RUNNING_KEY):
- print(f"{PREFIX} Selection is already running.")
- return
- if bpy.context.mode != "OBJECT":
- print(f"{PREFIX} Switch to Object Mode first.")
- return
- objects = tuple(bpy.context.selected_objects)
- if not objects:
- print(f"{PREFIX} Select objects first.")
- return
- window = bpy.context.window
- contexts = list(outliner_contexts(window)) if window else []
- if not contexts:
- print(f"{PREFIX} Open a View Layer Outliner in this window.")
- return
- view_layer = bpy.context.view_layer
- targets = target_collections(objects, view_layer.layer_collection)
- if not targets or any(not targets.intersection(o.users_collection) for o in objects):
- print(f"{PREFIX} Some objects have no non-excluded containing collection.")
- return
- depths = [d for _, d in layer_collections(view_layer.layer_collection)]
- depth = max(depths, default=0)
- original_active = view_layer.objects.active
- original_collection = view_layer.active_layer_collection
- print(f"{PREFIX} Blender {bpy.app.version_string}; {len(objects)} objects -> "
- f"{len(targets)} collections: {', '.join(sorted(c.name for c in targets))}")
- def work():
- try:
- # Deselect BEFORE row selection so later object updates cannot
- # overwrite it. Never substitute the active object for the target set.
- for obj in objects:
- obj.select_set(False)
- view_layer.objects.active = None
- yield
- for override in contexts:
- yield from select_rows(override, targets, depth)
- print(f"{PREFIX} Verified {len(targets)} selected collection rows "
- f"in {len(contexts)} Outliner(s); objects deselected.")
- except Exception as error:
- for obj in objects:
- obj.select_set(True)
- view_layer.objects.active = original_active
- view_layer.active_layer_collection = original_collection
- print(f"{PREFIX} FAILED: {error}. Original object selection restored. "
- "Outliner rows/expansion may have changed.")
- finally:
- bpy.app.driver_namespace.pop(RUNNING_KEY, None)
- iterator = work()
- def step():
- try:
- next(iterator)
- return 0.1
- except StopIteration:
- return None
- except Exception as error:
- import traceback
- print(f"{PREFIX} TIMER FAILED: {error}")
- traceback.print_exc()
- bpy.app.driver_namespace.pop(RUNNING_KEY, None)
- return None
- bpy.app.driver_namespace[RUNNING_KEY] = True
- bpy.app.timers.register(step, first_interval=0.1)
- if __name__ == "__main__":
- main()
Advertisement