3
$\begingroup$

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?

$\endgroup$

4 Answers 4

4
$\begingroup$

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

$\endgroup$
1
  • $\begingroup$ Works great! Thank you! $\endgroup$ Nov 12, 2019 at 13:37
11
$\begingroup$

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

$\endgroup$
6
  • 1
    $\begingroup$ 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. $\endgroup$ Nov 12, 2019 at 13:36
  • 2
    $\begingroup$ 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" $\endgroup$
    – batFINGER
    Nov 12, 2019 at 14:03
  • $\begingroup$ Thank you for the explanation. I think "accepted" makes more sense :) $\endgroup$ Nov 12, 2019 at 14:17
  • $\begingroup$ @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) $\endgroup$
    – Gorgious
    Feb 3, 2021 at 19:39
  • 1
    $\begingroup$ Alright, awesome ! :) $\endgroup$
    – Gorgious
    Feb 4, 2021 at 15:25
1
$\begingroup$

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.

$\endgroup$
1
$\begingroup$

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)
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .