Okay, if what user Gorgious commented on my question is indeed true, there likely isn't a direct way to assign a vertex to a variable. At least not in a stable way.
I would still appreciate to be proven wrong though.
So I decided to switch to an indirect way, which is:
We give vertices some kind of tag, and then later identify them by their tag again. This isn't optimal, but it does work for instance even if the vertex indices change.
Note however that vertex objects don't support custom properties.
Below you can see code for a class I wrote that tags vertices by assigning them individual weights in a (new) Vertex Group. It hasn't experienced much testing and looks a bit complicated, but it can work correctly and has documentation included.
import bpy
import math
C = bpy.context
D = bpy.data
class tag_vertices:
"""
Helps keeping track of indivual mesh vertices by creating a vertex group and "tagging" them in it via individual weights
Unlike with most other objects in Blender, it seems like you cannot assign vertices to variables in a stable way. Once you switch to Edit and then back to Object mode, they lose their validity.
See https://developer.blender.org/T88914 for more on that.
To circumvent this, this class assigns each vertex an individual weight in a newly created vertex group and is then able to later identify the origianl "identity" of a vertex via its weight.
...
Attributes
----------
obj : bpy.types.Object
The object whose vertices are being dealt with
VG : bpy.types.VertexGroup
The vertex group in which the vertices have been assigned individual weights
dicts : list of dictionaries
every dictionary is from different points of time, with the layout {weight_of_vertex: vertex_index or None}
increment : float
Used by class functions; is the smallest allowed difference between weights; is based on the total amount of vertices in the mesh
highest_digit : int
Used by class functions; position of the first digit after the decimal of increment that isn't a zero (increment == 0.0001 -> highest_digit == 4)
Methods
-------
create():
Called automatically at the creation of a class instance. Assigns weights to all vertices in VG and saves first dictionary in dicts. Do not use it yourself.
add_current():
Checks which vertex currently has which weight in VG and saves resulting dictionary in dicts attribute.
compare_indices(old_dict = 'DEFAULT', new_dict = 'DEFAULT'):
Compares two dictionaries of attribute dicts and returns a dictionary with the following layout: {old_vertex_index: new_vertex_index}
truncate_decimals(number, highest_digit):
takes in a float number and returns same number with all digits after the highest_digit cut off. Used by other class methods.
get_highest_digit(float_number):
takes in a float number and returns the position of the first digit that isn't a zero . Used by other class methods.
"""
#
def __init__(self, object = 'ACTIVE', VG_name = 'tagged_vertices'):
"""
Creates object attributes, VG and calls create(). Cannot be called from edit mode
Parameters
----------
object : bpy.types.Object OR 'ACTIVE'
The object that gets the new Vertex Group. If == 'ACTIVE', defaults to the currently active object.
VG_name : str
The desired name of the new Vertex Group.
"""
#
if C.mode == 'EDIT_MESH':
raise RuntimeError('__init__ of tag_vertices class cannot be run while in Edit Mode')
if object == 'ACTIVE':
object = C.object
self.obj = object
VG_i_active = self.obj.vertex_groups.active_index #using active instead of active_index is possible too, but assigning active = None crashes Blender as of version 2.93.2
self.VG = self.obj.vertex_groups.new(name = VG_name)
self.obj.vertex_groups.active_index = VG_i_active #setting the previously active VG as active again, as creating a new one makes the new one active
self.dicts = []
self.create()
#
def create(self):
"""
Called at the creation of a class instance. Assigns weights to all vertices in VG and saves first dictionary {weight: vertex_index} in dicts attribute.
"""
#
t_dict = {}
#VG weights only accept values between 0 and 1, so we need to calculate how big our increment needs to be to not reach that weight of 1 with a vertex.
self.increment = 1/len(self.obj.data.vertices) #since weights in a VG can only be between 0.0 and 1.0, we need to calculate the smallest "distance" between weights we should use.
self.highest_digit = self.get_highest_digit(self.increment)
self.increment = self.truncate_decimals(self.increment, self.highest_digit) #an increment of 0.00384123 gets changed to 0.003, rounding up might mean that the last assigned vertex has a weight over 1.0
weight = 0 #first vertex gets a weight of 0
for index in range(len(self.obj.data.vertices)):
self.VG.add([index], weight, 'ADD')
t_dict.update({weight: index}) #using floats as keys might lead to problems due to how floats "actually" look, but that's a problem for future-us. Other methods might increase calculating time of this for-loop.
weight = round(weight + self.increment, self.highest_digit) #floats behave weirdly sometimes, so 0.003 + 0.001 might actually give you 0.00399999999, which we don't want.
#we must not use truncating on anything other than calculacting the increment value, since truncating a value like 0.00399999 returns 0.003
self.dicts.append(t_dict)
#Vertex Groups dont support custom properties, so we cannot save the increment to the VG object
#
def add_current(self):
"""
Checks which vertex currently has which weight in VG and saves resulting dictionary {weight: vertex_index} in attribute dicts.
"""
#
t_dict = {}
for index in range(len(self.obj.data.vertices)):
try:
weight = self.VG.weight(index)
t_dict.update({round(weight, self.highest_digit): index})
except: #VG.weight(index) gives an error if vertex isn't in that VG
None
self.dicts.append(t_dict)
#
def compare_indices(self, old_dict = 'DEFAULT', new_dict = 'DEFAULT'):
"""
Compares two dictionaries of dicts and returns a dictionary with the following layout: {old_vertex_index: new_vertex_index, etc.}
Parameters
----------
old_dict : dictionary
an older dictionary from attribute dicts. You most likely want to use the one that's from the time of creation, AKA the first in the list.
new_dict : dictionary
a newer dictionary from attribute dicts.
Returns
-------
t_dict : dictionary
{old_vertex_index: new_vertex_index or None, etc.}
"""
#
if old_dict == 'DEFAULT':
old_dict = self.dicts[0]
if new_dict == 'DEFAULT':
new_dict = self.dicts[1]
t_dict = {}
for weight in old_dict:
old_index = old_dict[weight]
try:
new_index = new_dict[weight]
except:
new_index = None
t_dict.update({old_index: new_index})
return t_dict
#
def truncate_decimals(self, number, highest_digit): #truncating is the process of basically cutting off all numbers after a certain position in a number. The default math.trunc() function is only able to delete *all* decimals, so we have to make our own custom one to work with certain decimal places.
"""takes in a float number and returns same number with all digits after the highest_digit cut off"""
multiplier = 10**highest_digit
circumsized = math.trunc(number * multiplier) / multiplier
return circumsized #lol
#
def get_highest_digit(self, float_number): #returns the position of the "largest" number in a float. 0.00052013 would return 4, 0.00000123 would return 6.
"""takes in a float number and returns the position of the first digit that isn't a zero"""
pos = 0
while True:
pos += 1
if self.truncate_decimals(float_number, pos) > 0:
break
return pos
If I were to use that class in the example from my original question, it would look like this:
import bpy
C = bpy.context
D = bpy.data
obj = C.object #using C.object.data is not recommended since it can be unassigned by Blender
select_states = {}
for vert in obj.data.vertices:
select_states.update({vert.index: vert.select}) #{0: True, 1: True, 2: False, etc.}
t_verts = tag_vertices()
#do some stuff in viewport, such as deselecting, moving or deleting vertices, switching modes, etc.
t_verts.add_current() #needed to compare
old_vs_new = t_verts.compare_indices() #returns a dictionary with this layout {original_index: current_index or None(if deleted)}
changes = 0
#comparing selection states
for old_index in select_states:
new_index = old_vs_new[old_index]
if new_index == None:
changes += 1
else:
old_state = select_states[old_index]
new_state = obj.data.vertices[new_index].select
if old_state != new_state: #new state can also be None
changes += 1
print("a total of",changes,"vertices either changed their selection state or have been deleted")