3
$\begingroup$

Is the last object to be created always the last element of the bpy.data.objects list?

This wouldn't be necessary if all operators which create objects returned the object(s) created...

Right, so bpy.context.object doesn't seem to reliably work:

import bpy
o = bpy.data.objects['Cube']
# clear the selection
bpy.ops.object.select_all(action='DESELECT')
for i in [0,1,2]:
    #select the Cube
    o.select_set(True)    
    # duplicate it
    bpy.ops.object.duplicate()
    # this should be the new object (but it isn't)
    temp = bpy.context.object    
    #bpy.ops.object.mode_set(mode='EDIT') 
    #bpy.ops.object.mode_set(mode='OBJECT')
    # register what we've got
    print('temp:', temp)     
    # delete the new object            
    bpy.data.objects.remove(temp, do_unlink=True)    
    # clear the selection
    bpy.ops.object.select_all(action='DESELECT')

The first time through the loop, temp is an object. The second time, it's None.

I am aware that I can get a list of all the objects in the scene before and after creation of new objects, and compare them to find out what's new, but this is essentially giving up and saying that blender can't tell me what was just created, in addition to being inefficient.

Additionally, if I try to enter edit mode immediately after the duplication, it's not possible for some reason. In the context where I am doing this, it is necessary to enter edit mode to modify the created object, but perhaps that deserves a second topic.

$\endgroup$
2
  • $\begingroup$ Ah, bpy.context.object seems to change depending on what's selected. I need a way to get the last object created, regardless of selection. $\endgroup$ Commented Mar 14, 2021 at 16:56
  • $\begingroup$ Unless it's impossible to create an object without ending up with it selected? $\endgroup$ Commented Mar 14, 2021 at 17:05

4 Answers 4

6
$\begingroup$

For duplicate operator use context.selected_objects for result

Is the last object to be created always the last element of the bpy.data.objects list?

Lets run a very simple little test in python console to see: add an empty to the default scene

>>> D.objects[:]
[bpy.data.objects['Camera'], bpy.data.objects['Cube'], bpy.data.objects['Lamp']]

>>> bpy.ops.object.empty_add()
{'FINISHED'}

>>> C.object
bpy.data.objects['Empty']

>>> D.objects[:]
[bpy.data.objects['Camera'], bpy.data.objects['Cube'], bpy.data.objects['Empty'], bpy.data.objects['Lamp']]

The property list appears to be sorted alphabetically.

Use the context

This wouldn't be necessary if all operators which create objects returned the object(s) created...

In the context - operator paradigm, an operator only returns a status (eg success) by using and altering context. The result is gleened after running from the context. It is always the case that directly after running a create object operator, the new object is context.object eg.

bpy.ops.mesh.primitive_cube_add()
cube = context.object

bpy.ops.mesh.primitive_cone_add()
cone = context.object

print(cube.name, cone.name)

Your new question.

Firstly recommend never using bpy.data.objects["Cube"] since there is no assurance that it exists in which case it throws a keyerror or is linked to the context scene. The None state mentioned in question alludes to ob = context.object would be better, or by name scene.objects.get(name)

The question script example in question is creating states as discussed in Is the active object and zeroth selected object always the same?

For the case of the duplicate operator it returns result in context.selected_objects If the context.object is None or not selected it is not duplicated, else it becomes the new context object (first time thru loop)

First time thru loop, (ob is context object to begin) have context object and is only selected after deselect all.

Have new context object "temp" (and selected) after dupe.

Then you remove it. context object -> None. It does not magically revert to some default object, if you remove the context object context.object is None

End of loop status No context object and none selected... what is the expected result???

The first time through the loop, temp is an object. The second time, it's None.

Yep, exactly as expected. (if printing context object) Takes a while to grasp the concept of context.

Options.

The duplicate operator takes the selected objects, dupes them and returns as selected, simply

temp = bpy.context.selected_objects[0]

Overide

bpy.ops.object.duplicate(
        {"selected_objects" : [ob]}
        )

temp = context.selected_objects[0]

or

Since interested in one object can set as the context object at loop start. (as demonstrated in ideasman42's answer) (or end) with

context.view_layer.objects.active = ob

and

temp = context.object
#or
temp = context.selected_objects[0]

a case when context object and zeroth are the same.

Having no context object can affect how other operators, eg entering edit mode funtion. Depending on the script functionality is often good practice to leave context how we found it. eg reinstating the context object and selection.

API

There are more regular low level API methods, which return the object reference, eg adding an empty to context collection.

mt = bpy.data.objects.new("MT", None)
context.collection.objects.link(mt)

for the "CtGP's" duplicate case

for i in range(3):
    temp = ob.copy()

Subscribe to msgbus

Have left this in as, was my understanding of first incarnation of question, re finding the last object created with an add primitive object.

If the user has added an object with the UI and we wish to know the last one : Similarly to Origins to the down of the object by default and Auto Normals by default look for the active object changing and check the active operator.

It is the case that most operators that add an object have a bl_idname that ends in "_add"

Finding other objects created by addon operators that do not adhere to this, or added via other scripting technigues is going to be tricky, without constantly keeping track of scene.objects and notifying on change. (Will look for link as have answered in this regard before too)

Here is a test script that sets scene.last_object_created to the object last created using one of blenders add object operators.

import bpy
from bpy.props import PointerProperty

handle = object()

# Triggers when an object is made active
subscribe_to = bpy.types.LayerObjects, "active" # 


def notify_test(context):
    if (getattr(context.active_operator, "bl_idname", "").endswith("_add")
            ):
        if context.object.name in context.scene.objects:
            context.scene.last_object_created = context.object

bpy.msgbus.subscribe_rna(
    key=subscribe_to,
    owner=handle,
    args=(bpy.context,),
    notify=notify_test,
)

bpy.msgbus.publish_rna(key=subscribe_to)

bpy.types.Scene.last_object_created = PointerProperty(
        type=bpy.types.Object,
        )

Test in py console after adding text object.

>>> C.active_operator
<bpy_struct, Operator("OBJECT_OT_text_add") at 0x7fb73cc7ccc8>

>>> C.scene.last_object_created
bpy.data.objects['Text']

Another idea would be to make a collection of pointer properties to store more than 1 in some order.

Please also note that any use of context is referring to bpy.context

$\endgroup$
4
  • $\begingroup$ Sorry, it appears that bpy.context.object is very unreliable...is there a simple way to do this? I've edited my initial question with simple sample code. $\endgroup$ Commented Mar 15, 2021 at 6:38
  • $\begingroup$ I can't figure out the formatting for this site (how do you put in paragraph breaks in a comment?), so unfortunately this will be a bit hard to read. You say "Firstly never use bpy.data.objects["Cube"] as there is no assurance that it exists in which case it throws a keyerror or is linked to the context scene". The above code is meant to run in a fresh scene. So the cube exists. I am not sure why you would even bother to comment on that. You say: "Moving the goalposts." I am not moving the goalposts. I asked how to access the last object created. $\endgroup$ Commented Mar 15, 2021 at 8:17
  • $\begingroup$ You say it's bpy.context.object, when the above example clearly does not do this. You can see the object being created, and you can see that the code you suggested doesn't work. Each line is commented. I deselect everything. Then select the cube. Then duplicate it. Then record the duplicate object. Then report the duplicate. Then remove the duplicate object. The deselect everything. Repeat. I am not sure how this is 'expected behaviour'...clear as mud. $\endgroup$ Commented Mar 15, 2021 at 8:17
  • $\begingroup$ If the command is meant to be bpy.context.object, then it doesn't work in this instance. If there's no other command, I'll have to do as I reported: make a list of all objects in the scene before the creation of the new object, then make another list afterwards, and compare them. It's a bit appalling that this is what's required. $\endgroup$ Commented Mar 15, 2021 at 8:17
2
$\begingroup$

A direct answer to your question is no as objects don't have any kind of time-stamp or ID that's incremented for each new data-block.

Although the script you post can be made to work.


Your script sets selection but not active, this is why bpy.context.object isn't working for you, this works as you expect.

import bpy
view_layer = bpy.context.view_layer
o = bpy.data.objects['Cube']
# clear the selection
bpy.ops.object.select_all(action='DESELECT')
for i in [0,1,2]:
    # Select the Cube
    o.select_set(True)
    view_layer.objects.active = o
    # duplicate it
    bpy.ops.object.duplicate()
    # this should be the new object (but it isn't)
    temp = bpy.context.active_object
    bpy.ops.object.mode_set(mode='EDIT') 
    bpy.ops.object.mode_set(mode='OBJECT')
    # register what we've got
    print('temp:', temp)     
    # delete the new object            
    bpy.data.objects.remove(temp, do_unlink=True)    
    # clear the selection
    bpy.ops.object.select_all(action='DESELECT')

If you really want you could store objects before running operations, and compare it with all objects afterwards. While it wont give you exact order, you will know which objects have been created.

$\endgroup$
0
$\begingroup$

I really hope there's a better way to do this, as this is incredibly inefficient and cumbersome, but this is the only thing which has worked consistently for me, so far.

old_objects = bpy.data.objects.values()
# <any operation that creates a new object>
new_objects = bpy.data.objects.values()      
for object in new_objects:
     if object not in old_objects:
         new_object = object          
   

Example of this working (in a new scene):

import bpy
o = bpy.data.objects['Cube']
# clear the selection
bpy.ops.object.select_all(action='DESELECT')
for i in [0,1,2]:
    #select the Cube
    o.select_set(True)    
    # duplicate it    
    objects = bpy.data.objects.values()
    bpy.ops.object.duplicate()   
    # register what we've got
    new_objects = bpy.data.objects.values()      
    for object in new_objects:
        if object not in objects:
            new_object = object          
    temp = new_object   
    print('temp:', temp)     
    # delete the new object            
    bpy.data.objects.remove(temp, do_unlink=True)    
    # clear the selection
    bpy.ops.object.select_all(action='DESELECT')
$\endgroup$
6
  • $\begingroup$ Can you clarify what you're talking about? This example works, which you can test. The example you suggested doesn't work, which you can also test. I am not seeing any crashing and the behaviour in this example matches expected behaviour. $\endgroup$ Commented Mar 15, 2021 at 9:16
  • $\begingroup$ Why would you advise against using what works? Can you expand on what you mean by "use sets to make it more efficient"? $\endgroup$ Commented Mar 15, 2021 at 9:33
  • 1
    $\begingroup$ Agree with batFINGER, better practice is using get() on the objects in the actual scene C.scene.objects.get("Cube") and C.scene.objects instead of bpy.data.objects (the object data block). "A set is an unordered collection with no duplicate elements", see the docs: docs.python.org/3/tutorial/datastructures.html#sets $\endgroup$
    – brockmann
    Commented Mar 15, 2021 at 10:46
  • $\begingroup$ Thread is about retrieveing the last created object in a blender scene: the cube object is peripheral but noted. I am unsure how the python documentation for the set data type would inform how to acquire the last object created in a blender scene. Maybe you could explain in more detail. $\endgroup$ Commented Mar 15, 2021 at 11:10
  • 1
    $\begingroup$ No thread, this is a Q&A site driven by volunteers. Pretty confident comment, just tried to help but obviously there is no need to improve your code or learn about python. $\endgroup$
    – brockmann
    Commented Mar 15, 2021 at 11:52
0
$\begingroup$

A variant of the before-after comparison that I am using while in search for a more optimal solution is using sets for the actual comparison:

old_objects = bpy.data.objects.values()
# <any operation that creates a new object>
new_objects = bpy.data.objects.values()
new_object = set(new_objects) - set(old_objects)

Using the difference() method instead of the - operator makes turning old_objects into a set superfluous, as the method can use any iterable. It seems to be marginally faster.

new_object = set(new_objects).difference(old_objects)
$\endgroup$

You must log in to answer this question.

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