acoolrocket

Blender - Selected objects to its respective collection selected

Sep 11th, 2026
34
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.59 KB | None | 0 0
  1. """Convert selected objects to containing collection rows in View Layer Outliners.
  2.  
  3. Run in Object Mode. Collection branches are expanded; filters are restored.
  4. Selection runs over several redraws. Do not interact with Outliners until done.
  5. """
  6.  
  7. import bpy
  8.  
  9. PREFIX = "[Collection Activator]"
  10. RUNNING_KEY = "collection_row_selection_running"
  11.  
  12.  
  13. def layer_collections(root):
  14. if root.exclude:
  15. return
  16. # The view-layer root is the Outliner "Scene Collection" row, not a
  17. # user collection and must never be part of the replacement selection.
  18. for child in root.children:
  19. if child.exclude:
  20. continue
  21. yield child, 1
  22. for layer, depth in layer_collections(child):
  23. yield layer, depth + 1
  24.  
  25.  
  26. def target_collections(objects, root):
  27. """All direct memberships in this View Layer, deduplicated."""
  28. available = {layer.collection for layer, _ in layer_collections(root)}
  29. return {c for obj in objects for c in obj.users_collection if c in available}
  30.  
  31.  
  32. def outliner_contexts(window):
  33. for area in window.screen.areas:
  34. if area.type != "OUTLINER" or area.spaces.active.display_mode != "VIEW_LAYER":
  35. continue
  36. region = next((r for r in area.regions if r.type == "WINDOW"), None)
  37. if region:
  38. yield {"window": window, "area": area, "region": region}
  39.  
  40.  
  41. def selected_ids():
  42. return set(bpy.context.selected_ids)
  43.  
  44.  
  45. def box_row(region, y, mode):
  46. result = bpy.ops.outliner.select_box(
  47. "EXEC_DEFAULT", xmin=0, xmax=max(1, region.width - 1),
  48. ymin=y, ymax=y, mode=mode,
  49. )
  50. if "FINISHED" not in result:
  51. raise RuntimeError("Outliner box selection cancelled")
  52.  
  53.  
  54. def select_rows(override, targets, depth):
  55. """Box-select rows by Y, retaining only verified target collection IDs."""
  56. area, region = override["area"], override["region"]
  57. space = area.spaces.active
  58. properties = ("filter_text", "use_filter_object", "use_filter_collection",
  59. "use_filter_view_layers", "use_sync_select")
  60. saved = {name: getattr(space, name) for name in properties}
  61. try:
  62. space.use_sync_select = False
  63. space.filter_text = ""
  64. space.use_filter_object = False
  65. space.use_filter_collection = True
  66. space.use_filter_view_layers = False
  67. area.tag_redraw()
  68. yield
  69. with bpy.context.temp_override(**override):
  70. for _ in range(depth + 2):
  71. bpy.ops.outliner.show_one_level(open=True)
  72. area.tag_redraw()
  73. yield
  74. with bpy.context.temp_override(**override):
  75. bpy.ops.outliner.select_all(action="DESELECT")
  76.  
  77. previous = None
  78. for _ in range(10000):
  79. position = region.view2d.region_to_view(0, 0)[1]
  80. if previous is not None and abs(position - previous) < 0.01:
  81. break
  82. previous = position
  83. with bpy.context.temp_override(**override):
  84. bpy.ops.outliner.scroll_page(up=True)
  85. yield
  86. else:
  87. raise RuntimeError("Could not reach Outliner top")
  88.  
  89. previous = None
  90. for _ in range(10000):
  91. position = region.view2d.region_to_view(0, 0)[1]
  92. if previous is not None and abs(position - previous) < 0.01:
  93. break
  94. previous = position
  95. with bpy.context.temp_override(**override):
  96. for y in range(2, region.height - 2, 4):
  97. # Probe this row independently, including already selected
  98. # rows. Some structural rows expose no selected ID at all.
  99. box_row(region, y, "SUB")
  100. before = selected_ids()
  101. box_row(region, y, "ADD")
  102. added = selected_ids() - before
  103. if not added or added - targets:
  104. box_row(region, y, "SUB")
  105. if selected_ids() == targets:
  106. break
  107. bpy.ops.outliner.scroll_page(up=False)
  108. yield
  109. else:
  110. raise RuntimeError("Outliner page limit exceeded")
  111. with bpy.context.temp_override(**override):
  112. actual = selected_ids()
  113. if actual != targets:
  114. missing = ", ".join(sorted(c.name for c in targets - actual))
  115. raise RuntimeError(f"Collection rows not found: {missing}")
  116. finally:
  117. for name, value in saved.items():
  118. setattr(space, name, value)
  119. area.tag_redraw()
  120. yield
  121. with bpy.context.temp_override(**override):
  122. if selected_ids() != targets:
  123. raise RuntimeError("Selection changed after restoring filters/redrawing")
  124.  
  125.  
  126. def main():
  127. print(f"{PREFIX} Script started.")
  128. if bpy.app.driver_namespace.get(RUNNING_KEY):
  129. print(f"{PREFIX} Selection is already running.")
  130. return
  131. if bpy.context.mode != "OBJECT":
  132. print(f"{PREFIX} Switch to Object Mode first.")
  133. return
  134. objects = tuple(bpy.context.selected_objects)
  135. if not objects:
  136. print(f"{PREFIX} Select objects first.")
  137. return
  138. window = bpy.context.window
  139. contexts = list(outliner_contexts(window)) if window else []
  140. if not contexts:
  141. print(f"{PREFIX} Open a View Layer Outliner in this window.")
  142. return
  143. view_layer = bpy.context.view_layer
  144. targets = target_collections(objects, view_layer.layer_collection)
  145. if not targets or any(not targets.intersection(o.users_collection) for o in objects):
  146. print(f"{PREFIX} Some objects have no non-excluded containing collection.")
  147. return
  148. depths = [d for _, d in layer_collections(view_layer.layer_collection)]
  149. depth = max(depths, default=0)
  150. original_active = view_layer.objects.active
  151. original_collection = view_layer.active_layer_collection
  152. print(f"{PREFIX} Blender {bpy.app.version_string}; {len(objects)} objects -> "
  153. f"{len(targets)} collections: {', '.join(sorted(c.name for c in targets))}")
  154.  
  155. def work():
  156. try:
  157. # Deselect BEFORE row selection so later object updates cannot
  158. # overwrite it. Never substitute the active object for the target set.
  159. for obj in objects:
  160. obj.select_set(False)
  161. view_layer.objects.active = None
  162. yield
  163. for override in contexts:
  164. yield from select_rows(override, targets, depth)
  165. print(f"{PREFIX} Verified {len(targets)} selected collection rows "
  166. f"in {len(contexts)} Outliner(s); objects deselected.")
  167. except Exception as error:
  168. for obj in objects:
  169. obj.select_set(True)
  170. view_layer.objects.active = original_active
  171. view_layer.active_layer_collection = original_collection
  172. print(f"{PREFIX} FAILED: {error}. Original object selection restored. "
  173. "Outliner rows/expansion may have changed.")
  174. finally:
  175. bpy.app.driver_namespace.pop(RUNNING_KEY, None)
  176.  
  177. iterator = work()
  178.  
  179. def step():
  180. try:
  181. next(iterator)
  182. return 0.1
  183. except StopIteration:
  184. return None
  185. except Exception as error:
  186. import traceback
  187. print(f"{PREFIX} TIMER FAILED: {error}")
  188. traceback.print_exc()
  189. bpy.app.driver_namespace.pop(RUNNING_KEY, None)
  190. return None
  191.  
  192. bpy.app.driver_namespace[RUNNING_KEY] = True
  193. bpy.app.timers.register(step, first_interval=0.1)
  194.  
  195.  
  196. if __name__ == "__main__":
  197. main()
Advertisement
Comments
  • User was banned
Add Comment
Please, Sign In to add comment