3
$\begingroup$

Although I've read a Q&A for an issue very similar to mine (same error), I can't figure out what I need to change in my script to resolve it. I hate to post such a similar question, but I couldn't really take away from that what the "trick" of troubleshooting this type of error is.

I understand the cause in principle is that somewhere in the code, an attempt is being made to access data that no longer exists. What would be the steps for tracking down the offending code? I know from what the console messages that it occurs in at least one place: line 87, in check_for_matches and I have tried changing that function in lots of different ways, but without being able to pin down the data that is expected to exist but doesn't, I'm stuck.

What my script is supposed to do is cycle through all objects having names beginning with 'B' and join to them all objects having names beginning with 'L' that are close enough. "Close enough" is defined as having origins in the same locations as the circular geometry of the meshes, but I don't think it's necessary to know how that part of the code works to understand the problem. This error occurs in the process of joining the objects together.

Here is my code:

import bpy
import bmesh
import statistics
from mathutils import Matrix
from mathutils import Vector
import sys
import logging
logging.basicConfig(level=logging.DEBUG)

context = bpy.context
active_obj = context.view_layer.objects.active
bk_objs = []
lg_objs = []


def select_only(source_obj):
    bpy.ops.object.select_all(action='DESELECT') # deselect all
    source_obj.select_set(True) # select just the mesh source object
    bpy.context.view_layer.objects.active = source_obj # make this the active object


def select_starting_with(beginning_of_name):
    beg = beginning_of_name
    if type(beg) is str:
        all_objs = bpy.data.objects # all objects
        for obj in all_objs: # select all mesh objects with mesh data starting with a specific name
            if obj.type == 'MESH': # first make sure it's a mesh
                if obj.data.name.startswith(beg):
                    obj.select_set(True)
    else:
        print('Error: Expected string.')


def shift_origin(ob, x, y, z):
    mw = ob.matrix_world
    imw = mw.inverted()
    me = ob.data

    offset = Vector((x, y, z)) # offset to move origin in object's local space

    me.transform(Matrix.Translation( -offset )) # move mesh negatively by the offset
    ob.matrix_world.translation = mw @ offset # move object positively by the offset


def group_all_verts(object_name, group_name='Group', weight=1.0, type='REPLACE'): # type can be 'REPLACE' to set weight, 'ADD', or 'SUBSTRACT'
    o = object_name
    group = o.vertex_groups.new(name = group_name)
    vertices_to_add = []
    bpy.ops.object.mode_set(mode='OBJECT') # group.add(): cannot be called in Edit Mode
    for v in o.data.vertices:
        vertices_to_add.append(v.index)
    try:
        group.add(vertices_to_add, weight, type)
        logging.debug("\t\tvertex group added")
    except:
        logging.error(sys.exc_info())
        print('Object:', o.name, '| Group:', group.name, '| Vertices:', vertices_to_add, '| Object vertices could not be added to group.')


def assign_vertex_color(obj, vcol_map_name, rgba):
    bpy.ops.object.mode_set(mode='OBJECT')
    mesh = obj.data # mesh data
    mesh.vertex_colors.new(name = vcol_map_name) # make a vertex color map with this name
    color_layer = mesh.vertex_colors[vcol_map_name]
    i = 0
    for poly in mesh.polygons:
        for idx in poly.loop_indices:
            color_layer.data[i].color = rgba
            i += 1


def prepare_bks ():
    bpy.ops.object.select_all(action='DESELECT') # deselect all
    select_starting_with('B')
    sel_objs = bpy.context.selected_objects
    for obj in sel_objs:
        bk_objs.append(obj)


def check_for_matches(current_location):
    global lg_objs
    r = 4 # decimal places to round to
    cx = round(current_location[0], r)
    cy = round(current_location[1], r)
    cz = round(current_location[2], r)
    for lo in lg_objs:
        lx = round(lo.location[0], r)
        ly = round(lo.location[1], r)
        lz = round(lo.location[2], r)
        if cx == lx and cy == ly and cz == lz:
            joinable_lgs.append(lo)


def join_lgs_to_bk(current_bk):
    bpy.ops.object.select_all(action='DESELECT') # deselect all
    bpy.context.view_layer.objects.active = current_bk # make this the active object
    current_bk.select_set(True) # select the current BK object
    for jl in joinable_lgs: # select the joinable LG objects
        jl.select_set(True)
    bpy.ops.object.join() # join operation using active and selected objects


def check_geom(ob, facecount, match_area, decplaces=3):
    if ob.type == 'MESH': # make sure it's a mesh
        context.view_layer.objects.active = ob # must be active to enter Edit Mode
        bpy.ops.object.mode_set(mode='EDIT')
        me = ob.data
        bm = bmesh.from_edit_mesh(me).copy() # bmesh as a COPY
        bm.transform(ob.matrix_world) # that copy gets converted to world coords

        poles = [
            v for v in bm.verts 
            if len(v.link_faces) == len(v.link_edges) == facecount
            and all(len(f.edges) == 3 for f in v.link_faces)
            ]
        print("Potential Poles", [p.index for p in poles])
        for p in poles:
            def check(f):
                return (
                    bool(
                        e for e in f.edges 
                        if p not in e.verts 
                        and e.is_boundary
                        )
                    and
                        all(
                        len(e.other_vert(p).link_edges) == 3
                        for e in p.link_edges
                        )
                    )
            area = sum(f.calc_area() for f in p.link_faces)
            if all(check(f) for f in p.link_faces):
                print('\n' + str(p.index) + " is a " + str(facecount) + " tri fan pole, with an area of " + str(area) )
            if round(area, decplaces) == round(match_area, decplaces): # things to do if the area matches
                bpy.ops.object.mode_set(mode='OBJECT')
                v_x = []
                v_y = []
                v_z = []
                
                linked_verts = [p] + [e.other_vert(p) for e in p.link_edges] # verts of this island
                
                for v in linked_verts:
                    # populate lists with coordinates for each axis
                    v_x.append(v.co.x)
                    v_y.append(v.co.y)
                    v_z.append(v.co.z)
                    
                # average each list's values to find the center
                c_x = statistics.mean(v_x)
                c_y = statistics.mean(v_y)
                c_z = statistics.mean(v_z)
                center = [c_x, c_y, c_z] # mean of island vertices
                print('Center location: ' + str(center) )
                
                # actions to take upon finding the circle centers
                check_for_matches(center)
    else:
        print('Cannot proceed. Active object is not a mesh.')


def prepare_lgs():
    '''
    === This function will: ===
    ・Find LG objects by name
    ・Shift their object origins
    ・Group their vertices
    ・Assign a vertex color
    '''
    print('\n====== Attaching lgs ======')
    bpy.ops.object.select_all(action='DESELECT') # deselect all
    select_starting_with('L') # select objects with names starting with...
    
    sel_objs = bpy.context.selected_objects # all selected objects
    if len(sel_objs) > 0: # if any objects were selected (more than 0)
        for obj in sel_objs: # do the following for all objects
            select_only(obj) # select only this current object
            shift_origin(obj, 0, 0, 0.074783) # shift their object origins by (distances in local XYZ)
            group_all_verts(obj, 'lg')
            assign_vertex_color(obj, 'lg Geometry', (1.0, 0.0, 0.0, 1.0) )
            lg_objs.append(obj)


prepare_lgs()
prepare_bks()

for bk in bk_objs:
    joinable_lgs = []
    check_geom(bk, 48, 0.045) # params: the object, the face count, the total face area (object must be a triangle fan mesh)
    join_lgs_to_bk(bk)

bpy.ops.object.mode_set(mode='OBJECT')
try: # make active again the object the user had initially had as active
    context.view_layer.objects.active = active_obj
except: # unless it no longer exists because it was joined to another object
    print('Could not find previously active object.')

If someone could please explain how to track down which data causes ReferenceError: StructRNA of type Object has been removed, and how to construct a workaround, that would be much appreciated.

Since the script does need to detect some specific geometry, here's the .blend file: (739KB)

$\endgroup$
3
  • 1
    $\begingroup$ Have used an edit mode bmesh only for way of example in answer re checking matchesl. Using object mode is going to be a lot quicker and avoids unneeded edit mode toggle.. Could replace all operators here with low level calls. bm.from_mesh(me) used again in a bmesh is the (non modified) equivalent of joining a mesh. Henceforth feel this could be done very differently. The error is most likely an issue in the object lists via reference. Adding print methods is a way to debug. (Find run script in console addon (codemanx, sambler and pinkvertex example here) is another way to debug. $\endgroup$
    – batFINGER
    Dec 24, 2020 at 10:30
  • $\begingroup$ Often see us prattle on about XY Problem Feel this is a classic case. Would far prefer to answer how to get to desired result. $\endgroup$
    – batFINGER
    Dec 24, 2020 at 10:58
  • $\begingroup$ @batFINGER Thank you for those suggestions! I might not get a chance to really experiment with trying different approaches until after the holiday season. I am open to reaching the goal by other methods. I like the idea of avoiding operators, but don't always have the know-how to do it. I'm still pretty new to Python, but recently I've been enjoying it a lot as I take on projects and learn from this community. As for XY, my thinking is "I'd like to be able to solve this type of problem next time it comes up", even if I end up using a different approach this time. Happy holidays & new year! $\endgroup$
    – Mentalist
    Dec 25, 2020 at 8:39

1 Answer 1

5
$\begingroup$

Reference to object since removed.

Without going into debugging where this is happening in your code, it is most likely in looping one of the lists referencing objects that are subsequently removed, or no longer the same instance.

Simple test scenario, 3 meshes selected are added to a list. The 3 objects are joined, after which only the new joined up context object is valid.

Any attempt to access an invalid object reference results in the error shown (or worse). A good question would be how to efficiently determine this.

Simple example. All cube meshes in scene are added to a list. Then they are joined (assume one active and all selected).

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

>>> cubes = C.selected_objects
>>> cubes
[bpy.data.objects['Cube'], bpy.data.objects['Cube.001'], bpy.data.objects['Cube.002']]

>>> cubes = [cube for cube in C.scene.objects if cube.type == 'MESH' and cube.name.startswith("Cube")]

>>> cubes
[bpy.data.objects['Cube'], bpy.data.objects['Cube.001'], bpy.data.objects['Cube.002']]


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

>>> cubes
[bpy.data.objects['Cube'], <bpy_struct, Object invalid>, <bpy_struct, Object invalid>]

>>> 
>>> for cube in cubes:
...     try:
...         cube, cube.location
...         
...     except ReferenceError as e:
...         e
...         
(bpy.data.objects['Cube'], Vector((0.0, 0.0, 0.0)))
ReferenceError('StructRNA of type Object has been removed')
ReferenceError('StructRNA of type Object has been removed')

To resolve

As alluded to in comments, "How I would do it" differs a lot! So as requested via comment to avoid in future

Avoid making lists of references to objects, in particular Do not keep a reference to any object that you are removing. A couple of options to consider.

  • Remove all of the "joinee" objects from list before running join operator.
  • Generate the list each time.
$\endgroup$
2
  • $\begingroup$ I also have this question but my sence might be a little bit different. I don't know how to solve it. $\endgroup$
    – P. Scotty
    Jul 10, 2022 at 3:34
  • $\begingroup$ In my case I had a misspelled python variable which referred to an earlier loaded Blender project. This caused the same error because the object was freed when changing projects. (I am working in a jupyter notebook.) $\endgroup$ Feb 27, 2023 at 11:32

You must log in to answer this question.

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