Bpy indexes new vertices randomly upon different executions of my script

I’m using a script to generate erlenmeyer flasks with different dimensions and I’ve run into a problem:

When you create new vertices (especially when you make loop cuts and bevels) the new vertices will be indexed in the object data inconsistently. So let’s say I have a circle with 20 verts. Those verts will have the indices 0-19, obviously. If I extrude that upwards the new vertices will have the indices 20-39.

But the more you do this, the more geometry you add, the more likely it becomes that the indexing becomes inconsistent and random between different executions of the script. Watch the difference between this image
image

And this one

The code is completely identical. Those are just two separate executions. I’ve had this problem in the past and decided that this just means that blender’s bpy is either
A) Bad
B) Too counterintuitive

Is there a more reliable way to select parts of the mesh? Is there a way to prevent the random indexing?

Here’s my code:

import bpy


erlen_specs = '''V    R    r    h
25     		   42               22             75    
50     		   51               22             90    
100    		   64               22             105   
200    		   79               34             131   
250    		   85               34             145  
300    		   87               34             156  
500    		   105              34             180  
1000   		   131              42             220  
2000   		   166              50             280  
3000   		   187              52             310   
5000   		   220              51             365   '''

erlen_specs = [i.split() for i in erlen_specs.split('\n')]
erlen_specs = [dict(zip(erlen_specs[0],[int(k) for k in erlen_specs[i]])) for i in range(1,len(erlen_specs))]

print(erlen_specs)

def get_override():
        
    '''Obtains the current overwrite for a scene...or something.'''
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            for region in area.regions:
                if region.type == 'WINDOW':
                    override = {'area': area, 'region': region, 'edit_object':bpy.context.edit_object}
        
    return override       
    
def selection(i,j,sele):
        
    '''Select verts based on index.'''
        
    bpy.ops.object.mode_set(mode = 'OBJECT')
    obj = bpy.context.active_object
    bpy.ops.object.mode_set(mode = 'EDIT') 
    bpy.ops.mesh.select_mode(type="VERT")
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.mode_set(mode = 'OBJECT')
        
    if sele == 'verts':
        for v in range(i,j):
            obj.data.vertices[v].select = True
    elif sele == 'faces':
        for v in range(i,j):
            obj.data.polygons[v].select = True
    elif sele == 'edges':
        for v in range(i,j):
            obj.data.edges[v].select = True
        
    bpy.ops.object.mode_set(mode = 'EDIT') 



def sele_list(lst,sele):
        
    '''Select verts in lst.'''
        
    bpy.ops.object.mode_set(mode = 'OBJECT')
    obj = bpy.context.active_object
    bpy.ops.object.mode_set(mode = 'EDIT') 
    bpy.ops.mesh.select_mode(type="VERT")
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.mode_set(mode = 'OBJECT')
        
    if sele == 'verts':
        for v in lst:
            obj.data.vertices[v].select = True
    elif sele == 'faces':
        for v in lst:
            obj.data.polygons[v].select = True
    elif sele == 'edges':
        for v in lst:
            obj.data.edges[v].select = True
        
    bpy.ops.object.mode_set(mode = 'EDIT') 



def make_erlen(R,r,h,V):
    
    # add circle bottom, set to R
    bpy.ops.mesh.primitive_circle_add(vertices=20,radius=R, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
    
    # fill circle
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.edge_face_add()
    
    # extrude, don't move
    bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "use_dissolve_ortho_edges":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, 0), "orient_axis_ortho":'X', "orient_type":'GLOBAL', "orient_matrix":((0, 0, 0), (0, 0, 0), (0, 0, 0)), "orient_matrix_type":'GLOBAL', "constraint_axis":(False, False, False), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "view2d_edge_pan":False, "release_confirm":False, "use_accurate":False, "use_automerge_and_split":False})
    
    # move the new vertices along the z axis by h    
    bpy.ops.transform.translate(value=(0, 0, h), orient_axis_ortho='X', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)
    
    # scale new vertices down by r/R
    bpy.ops.transform.resize(value=(r/R, r/R, r/R), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)

    # make new vertices to extrude
    bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "use_dissolve_ortho_edges":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, 0), "orient_axis_ortho":'X', "orient_type":'GLOBAL', "orient_matrix":((0, 0, 0), (0, 0, 0), (0, 0, 0)), "orient_matrix_type":'GLOBAL', "constraint_axis":(False, False, False), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "view2d_edge_pan":False, "release_confirm":False, "use_accurate":False, "use_automerge_and_split":False})
    
    # pull up by fac
    fac = 0.7*(h/2)**(1/2)
    bpy.ops.transform.translate(value=(0, 0, fac), orient_axis_ortho='X', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)
    
    # delete top face
    bpy.ops.mesh.delete(type='FACE')
    
    # apply scale
    bpy.ops.object.editmode_toggle()
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    bpy.ops.object.editmode_toggle()
    
    # select bottom ring
    obj = bpy.context.active_object
    a,b = 0,20
    selection(a,b,'verts')
    
    # bevel bottom
    bpy.ops.mesh.bevel(offset=0.08, offset_pct=0, segments=2, affect='EDGES')
    
    # select top ring
    obj = bpy.context.active_object
    a,b = 0,20
    selection(a,b,'verts')
    
    # bevel middle
    bpy.ops.mesh.bevel(offset=0.04, offset_pct=0, segments=2, affect='EDGES')
    
    # select bottom ring
    obj = bpy.context.active_object
    inds = [21,23,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,77]
    sele_list(inds,'verts')
    
    # inset bottom face
    bpy.ops.mesh.inset(thickness=0.1, depth=0)
    bpy.ops.mesh.inset(thickness=0.1, depth=0)
    bpy.ops.mesh.poke()
    
    
    # go object mode
    bpy.ops.object.editmode_toggle()
    bpy.ops.object.shade_smooth()
    bpy.ops.object.modifier_add(type='SOLIDIFY')
    bpy.context.object.modifiers["Solidify"].thickness = 0.01
    
    # apply solidify
    bpy.ops.object.modifier_apply(modifier="Solidify")

    # add edge loop
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.loopcut_slide(get_override(),MESH_OT_loopcut={"number_cuts":1, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":236, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":-0.75, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})
    bpy.ops.mesh.loopcut_slide(get_override(),MESH_OT_loopcut={"number_cuts":1, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":246, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":0, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})
    
    # scale middle one
    bpy.ops.transform.resize(value=(1.2, 1.2, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, True, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)
    return
    # loopcut that will sharpen the edge
    bpy.ops.mesh.loopcut_slide(get_override(),MESH_OT_loopcut={"number_cuts":1, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":541, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":-0.970203, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})
    
    # add subdiv
    bpy.ops.object.editmode_toggle()
    bpy.ops.object.subdivision_set(level=1, relative=False)

    bpy.context.object.name = str(V) +' mL'

    
    

for i in range(len(erlen_specs))[:1]:
    R = erlen_specs[i]['R']/100
    r = erlen_specs[i]['r']/100
    h = erlen_specs[i]['h']/100
    V = erlen_specs[i]['V']
    make_erlen(R,r,h,V)

Yeah you are correct, there is no point to chase vertex indexes because their implementation is far more complex behind the scenes. Rather than consider them an array of elements (or an std::vector list of vertices) is more like they are dynamically allocated here and there and then reiterated, hence you end up with incosistent or non-linear numbering. (Note that I don’t know how the internal implementation is written in C – but as I say is more complex rather than a simple one).

The next step that makes sense from now on is to consider to attach custom property to the vertex datatype. But after that I don’t know exactly how you can use this?

Say for example you keep incrementing a global integer counter each time vertices are added. You won’t have 100% perfect indexing but at least it will be an incremenetal one towards one direction. Then once you drop this custom-index on a global dictionary with `ids = { customindex : VertexDataType } you will be able to easily access the vertices that way instead of doing it directly from the Blender API. Theoretically in mind it would work, but I have not tried it myself to be sure.

If there are any other better ideas let us hear more of them… :slight_smile:

I would recommend approaching this in a different manner. Using a curve object you can create variations of the curve easily then use a spin and solidify modifier.

While inserting new points into a curve will modify the indexes of points that come after you always have a starting point as a reference and can potentially map based on height since your curve will essentially be 2d only.

here’s an old script i did from a similar approach.

import bpy
from random import random
from math import radians
from mathutils import Color


def gen_mesh(k):
    """
    param: k - integer from loop control
    return: objVase - object
    """
    verts = []
    edges = []
    faces = []
    x = y = z = 0
    verts.append([x, y, z])
    for i in range(0, 5):
        y = random()/2
        x = i/4
        verts.append([x, y, z])
    for i in range(0, 5):
        edges.append([i, i+1])
    meshVase = bpy.data.meshes.new("Vase"+str(k))
    objVase = bpy.data.objects.new("Vase"+str(k), meshVase)
    bpy.context.view_layer.active_layer_collection.collection.objects.link(objVase)
    meshVase.from_pydata(verts, edges, faces)
    meshVase.update(calc_edges=True)
    bpy.context.view_layer.objects.active = objVase
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.spin(steps=24, angle=radians(360), center=(0, 0, 0),
                      axis=(1, 0, 0))
    bpy.ops.mesh.remove_doubles()
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')
    return objVase


def add_modifiers(k, objVase):
    """
    param: k - integer from loop control
    param: objVase - object
    """
    objVase.modifiers.new('solidVase'+str(k), type="SOLIDIFY")
    objVase.modifiers['solidVase'+str(k)].thickness = 0.025
    objVase.modifiers.new('subdVase'+str(k), type="SUBSURF")
    objVase.modifiers['subdVase'+str(k)].render_levels = 4
    objVase.modifiers['subdVase'+str(k)].levels = 3


def gen_mat(k, objVase):
    """
    param: k - integer from loop control
    param: objVase - object
    """
    matVase = bpy.data.materials.new(name="VaseMat"+str(k))
    objVase.data.materials.append(matVase)
    matVase.use_nodes = True
    nodesVase = matVase.node_tree.nodes
    nodesVase.get('Principled BSDF').name = 'Prip'
    nodesVase.get('Prip').location = (0.0, 0.0)
    nodesVase.get('Material Output').location = (300.0, 0.0)
    col = nodesVase.new('ShaderNodeValToRGB')
    col.location = (-300.0, 0.0)
    col.name = 'Col1'
    col.color_ramp.elements[0].position = 0.25
    col.color_ramp.elements[1].position = 0.75
    col.color_ramp.elements.new(0.5)
    c1 = Color()
    c1.hsv = [random(), 1.0, 1.0]
    col.color_ramp.elements[1].color = [c1.r, c1.g, c1.b, 1.0]
    c1.v = 0.3
    col.color_ramp.elements[0].color = [c1.r, c1.g, c1.b, 1.0]
    col.color_ramp.elements[2].color = [c1.r, c1.g, c1.b, 1.0]
    matVase.node_tree.links.new(col.outputs[0],
                                nodesVase.get('Prip').inputs[0])
    nodesVase.new('ShaderNodeTexGradient')
    nodesVase.get('Gradient Texture').name = 'Grad1'
    nodesVase.get('Grad1').location = (-500.0, 0.0)
    matVase.node_tree.links.new(nodesVase.get('Grad1').outputs[0],
                                col.inputs[0])
    nodesVase.new('ShaderNodeTexCoord')
    nodesVase.get('Texture Coordinate').name = 'TexCoor1'
    nodesVase.get('TexCoor1').location = (-700.0, 0.0)
    matVase.node_tree.links.new(nodesVase.get('TexCoor1').outputs[3],
                                nodesVase.get('Grad1').inputs[0])
    nodesVase.new('ShaderNodeBump')
    nodesVase.get('Bump').name = 'Bmp1'
    nodesVase.get('Bmp1').location = (-300.0, -500.0)
    nodesVase.get('Bmp1').inputs[0].default_value = 0.15
    matVase.node_tree.links.new(nodesVase.get('Bmp1').outputs[0],
                                nodesVase.get('Prip').inputs['Normal'])
    col = nodesVase.new('ShaderNodeValToRGB')
    col.name = 'Col2'
    col.location = (-600.0, -500.0)
    matVase.node_tree.links.new(col.outputs[0],
                                nodesVase.get('Bmp1').inputs[2])
    nodesVase.new('ShaderNodeTexNoise')
    nodesVase.get('Noise Texture').name = 'Noise1'
    nodesVase.get('Noise1').location = (-800.0, -500.0)
    nodesVase.get('Noise1').inputs[2].default_value = 15.0
    matVase.node_tree.links.new(nodesVase.get('Noise1').outputs[0],
                                col.inputs[0])
    nodesVase.new('ShaderNodeSeparateXYZ')
    nodesVase.get('Separate XYZ').name = 'SeprXYZ1'
    nodesVase.get('SeprXYZ1').location = (-1000.0, -500.0)
    matVase.node_tree.links.new(nodesVase.get('SeprXYZ1').outputs[0],
                                nodesVase.get('Noise1').inputs[0])
    matVase.node_tree.links.new(nodesVase.get('TexCoor1').outputs[3],
                                nodesVase.get('SeprXYZ1').inputs[0])
    col = nodesVase.new('ShaderNodeValToRGB')
    col.name = 'Col3'
    col.location = (-300.0, -250.0)
    matVase.node_tree.links.new(col.outputs[0],
                                nodesVase.get('Prip').inputs[7])
    matVase.node_tree.links.new(nodesVase.get('Noise1').outputs[0],
                                col.inputs[0])
    col.color_ramp.elements[0].position = 0.4
    col.color_ramp.elements[1].position = 0.6


def main():
    for k in range(0, 10):
        objVase = gen_mesh(k)
        add_modifiers(k, objVase)
        objVase.location = (0, k*2, 0)
        objVase.rotation_euler[1] = radians(-90)
        polys = objVase.data.polygons
        for p in polys:
            p.use_smooth = True
        gen_mat(k, objVase)

main()