3

Is it possible to duplicate a collection from a Python script? When I do it manually, the Console shows bpy.ops.outliner.collection_duplicate(), however if I call it from script, there is a problem with context:

RuntimeError: Operator bpy.ops.outliner.collection_duplicate.poll() failed, context is incorrect

Does that mean that collection_duplicate() can be called only from the Outliner editor? If so, how to 'fake' it from a script?

brockmann
  • 12,613
  • 4
  • 50
  • 93
user1566515
  • 347
  • 4
  • 16

4 Answers4

11

API method

If you know the collection you wish to dupe, and the collection you wish to parent to, consider something like below. Because a collection can have many parents, I'm not totally sure without using the outliner how to determine which instance of the collection is being duped, and hence where in the hierarchy to paste.

As a test, I've copied the context collection to to the scene collection, with and without linked data. Using a method that recursively creates a new collection and populates it with object copies from the source.

EDIT. Have added a look up table with original -> dupe to change a dupes parent to a dupe (if duped). Other things to consider here are driver variable targets, constraint objects, modifier objects.

import bpy
from collections import  defaultdict

def copy_objects(from_col, to_col, linked, dupe_lut): for o in from_col.objects: dupe = o.copy() if not linked and o.data: dupe.data = dupe.data.copy() to_col.objects.link(dupe) dupe_lut[o] = dupe

def copy(parent, collection, linked=False): dupe_lut = defaultdict(lambda : None) def _copy(parent, collection, linked=False): cc = bpy.data.collections.new(collection.name) copy_objects(collection, cc, linked, dupe_lut)

    for c in collection.children:
        _copy(cc, c, linked)

    parent.children.link(cc)

_copy(parent, collection, linked)
print(dupe_lut)
for o, dupe in tuple(dupe_lut.items()):
    parent = dupe_lut[o.parent]
    if parent:
        dupe.parent = parent


test call

context = bpy.context scene = context.scene col = context.collection print(col, scene.collection) assert(col is not scene.collection) parent_col = context.scene.collection

copy(scene.collection, col)

and linked copy

copy(scene.collection, col, linked=True)

Note

For a totally linked copy, ie the objects and collections within are linked copies then

cc = collection.copy()

will do the trick.

Related

Change active collection

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • 1
    Thank you for a detailed answer. I tested it and it works as described. I marked another answer as correct because it appeared earlier and is a valid answer to this particular question. I also like your answer because it is a good working example of how to manipulate objects and collections. – user1566515 Nov 12 '19 at 13:36
  • 2
    Cheers and No probs. On a purely semantics lilt: Despite the big green tick, it's "accepted" (by you) rather than being "correct" which implies other answers are unaccepted rather than incorrect lol. If I had my way would change SE such that any or all answers could be accepted. and one is "favoured" To quote RickyBlender "Happy Blending" – batFINGER Nov 12 '19 at 14:03
  • Thank you for the explanation. I think "accepted" makes more sense :) – user1566515 Nov 12 '19 at 14:17
  • @batFINGER Hey, someone on a related answer pointed out that while your solution copies a collection and all its objects, it doesn't retain the parent relationship between them. Do you think it would be possible to edit the answer to add it to the features ? (I may be able to do it if you don't mind) – Gorgious Feb 03 '21 at 19:39
  • 1
    Alright, awesome ! :) – Gorgious Feb 04 '21 at 15:25
  • Gorgious is right, it doesn't retain the parent relationship between them which gives impredictibles results, would be nice to fix this, thanks – cscholl Apr 18 '22 at 15:59
4

This will duplicate the active collection in the outliner:

import bpy

for window in bpy.context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'OUTLINER':
            override = {'window': window, 'screen': screen, 'area': area}
            bpy.ops.outliner.collection_duplicate(override)
            break

From the documentation example here: Execution Context

VVV
  • 276
  • 1
  • 4
1

After a lot of trial and error (mostly error) here is a routine I came up with to copy one collection to a new collection by name. You can copy the inner objects as linked objects with the third parameter.

def duplicateCollectionByName(origName,newName,linked=False):
    original_collection = bpy.data.collections[origName]
    new_collection = bpy.data.collections.new(newName)
    bpy.context.scene.collection.children.link(new_collection)
    new_index=len(bpy.context.scene.collection.children) 
    bpy.ops.object.select_all(action='DESELECT')
    for obj in original_collection.objects:
        obj.select_set(True)
    bpy.ops.object.duplicate_move_linked(OBJECT_OT_duplicate={"linked":linked, "mode":'TRANSLATION'})
    bpy.ops.object.move_to_collection(collection_index=new_index)
    return new_index

It is cool that when you perform actions in the Blender scene you can see the code spit out. Too bad it is wrong 90% of the time. Well, not "wrong", but it often uses commands that simply don't work inside python. I have performed tasks then copied/pasted that command into the console and most times it just throws an error. That is frustrating.

1

Here is a version I came up with. It preserves the hierarchy but does NOT copy the object data. Copy the data along the object and rebind them if you want a deep copy.

#=====================================
def clone_collection(src : Collection) -> Collection:
sc = bpy.context.scene

# create a new collection to collect copied objects
clone_collec = bpy.data.collections.new('clone')

# add the created collection to the scene
sc.collection.children.link(clone_collec)

# duplicates 'src' collection root objects recursively
for obj in src.objects:
    if (obj.parent == None):
        clone_object_recursive(obj, None, clone_collec)


#------- def clone_object_recursive(obj : Object, parent: Object, clone_collec : Collection):

copy = bpy.data.objects.new(obj.name, obj.data)
copy.parent = parent
copy.matrix_local = obj.matrix_local
clone_collec.objects.link(copy)

for child in obj.children:
    clone_object_recursive(child, copy, clone_collec)

feranti
  • 11
  • 3