Hello @ymoon,
Thank you for reaching out to us. Your posting is slightly ambiguous as you do not make fully clear what you would consider to be 'the last selected point'.
I assume that you mean last here in the sense of a temporal selection order, opposed to the point index selection order provided by PointObject.GetPointS
. I.e., you want to know which point has been selected last in time by the user or programmatically.
Cinema 4D does not store such data directly, and the data which is stored is volatile and non-trivial to access, as neither a PointObject
nor a BaseSelect
store a selection order over time. What you can do is:
- (Sort of recommended) Caching things yourself: Implement an entity which watches the scene selection state. You could use a
MessageData
plugin subscribed to a timer event or listening to EVMSG_CHANGE
for this. It would then track the point selection state of all objects in the scene in the temporal fashion you desire. There will be hoops to jump through:
- Identifying the same objects over time. See this posting.
- Throttling the computations of your plugin, both traversing the whole document on a shorter timer and for all
EVMSG_CHANGE
can become a bottleneck for a user. Limiting the tracking only to selected objects could be one way to throttle your plugin.
- (Not recommended) Unfolding undo stacks: An undo item of type
UNDOTYPE_CHANGE_SELECTION
represents a change of a selection state of points, polygons or edges. With BaseDocument.FindUndoPtr you could unwind the undo stack to retrace in which temporal order points have been selected.
- Although this might seem easier, I would expect more problems to occur here and this to be also the computationally heavier approach, as one would have to unfold and refold undo's.
Note that both approaches will suffer from:
- Potentially being a scene bottleneck by adding a lot of overhead. This can be avoided when implemented carefully for both approaches; or it can be simply ignored when only small or medium scenes are targeted.
- Being unable to track the temporal selection order for (point) selection states which have been loaded with the document. Caching things or going back in the undo stack also means that you first must have witnessed them happening.
I have provided a brief sketch below for the first option to clarify how such a thing could be done. Please understand that this is indeed a sketch and not a solution, you must implement the details yourself.
Cheers,
Ferdinand
Result:

Code:
"""Realizes a simple EVMSG_CHANGE based approach to track the temporal order in which point
selections have been created.
Must be run as a Script Manager script. Change the selection of point objects to see changes being
reported to the console. This sketch suffers from two problems any implementation of this task (I
can think of) will suffer from:
* It can be computationally expensive to do the tracking, which is here mitigated by only
updating the cache for selected objects.
* It is inherently unable to cross document loading boundaries. When an object is loaded in
with an already existing selection, it is not possible to deduce its temporal order.
"""
import c4d
import time
import copy
class SelectionStateCache:
"""Stores selection states (state, time) tuples of objects over their UUID.
The central method for feeding new data is SelectionStateCache.Update() which will add the
selection states of all selected point objects of the passed document. The type/method does not
check if it is always being fed with the same document
Update() deliberately only takes the selected point objects in a document into account since
traversing and caching the whole object tree in Python would add substantial execution time. The
selection state of not selected objects which have been added before will remain, it is only
state changes which will not be tracked when an object is not selected.
"""
@staticmethod
def GetUUID(node: c4d.C4DAtom) -> bytes:
"""Returns an UUID for #node which identifies it over reallocation boundaries.
"""
if not isinstance(node, c4d.C4DAtom):
raise TypeError(f"{node = }")
data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
if not isinstance(data, memoryview):
raise RuntimeError(f"Could not access UUID for: {node}")
return bytes(data)
def __init__(self) -> None:
"""Initializes a selection state cache.
"""
# Stores selection states in a scene in the form:
# {
# UUID_0: { // A point object hashed over its UUID.
# 0: (state, time), // The selection state and time of the point at index 0.
# 1: (state, time),
# ...
# },
# UUID_1: {
# 0: (state, time),
# 1: (state, time),
# ...
# },
# }
self._data: dict[bytes, dict[int, tuple[bool, float]]] = {}
self._isDirty: bool = False
def _update(self, node: c4d.PointObject) -> None:
"""Updates the cache with the point object #node.
"""
if not isinstance(node, c4d.PointObject):
raise TypeError(f"{node = }")
# Identify an object over its UUID so that we can track objects over reallocation boundaries,
# In 2023.2, doing this is technically not necessary anymore since C4DAtom.__hash__ has been
# added which does the same thing under the hood.
uuid: bytes = SelectionStateCache.GetUUID(node)
t: float = time.perf_counter()
count: int = node.GetPointCount()
# Get the current selection state and cached selection state for the object.
docState: dict[int, tuple[bool, float]] = {
n: (s, t) for n, s in enumerate(node.GetPointS().GetAll(count))}
cacheState: dict[int, tuple[bool, float]] = self._data.get(uuid, {})
# Update the cache when there is either None for a given point or when the selection state
# of the point has changed,
for pointIndex in docState.keys():
docValue: tuple[bool, float] = docState.get(pointIndex, tuple())
cacheValue: tuple[bool, float] = cacheState.get(pointIndex, tuple())
if not cacheValue or cacheValue[0] != docValue[0]:
cacheState[pointIndex] = docState[pointIndex]
self._isDirty = True
# Write the cache of #node.
self._data[uuid] = cacheState
def __getitem__(self, node: c4d.PointObject) -> dict[int, tuple[bool, float]]:
"""Returns a copy of the cache for #node.
"""
if not isinstance(node, c4d.PointObject):
raise TypeError(f"{node = }")
uuid: bytes = SelectionStateCache.GetUUID(node)
if not uuid in self._data.keys():
raise KeyError(f"The node {node} is not being tracked by the cache {self}.")
return copy.deepcopy(self._data[uuid])
@property
def IsDirty(self) -> bool:
"""Returns if the cache has changed since the last time this method has been called.
"""
res: bool = self._isDirty
self._isDirty = False
return res
def GetTemporallyOrderedSelection(self, node: c4d.PointObject) -> tuple[int]:
"""Returns the selection indices order over their selection time for #node.
This is the specific functionality asked for in https://plugincafe.maxon.net/topic/14519.
"""
data: list[tuple[int, float]] = [(k, v[1]) for k, v in self[node].items() if v[0]]
data.sort(key=lambda item: item[1])
return tuple(item[0] for item in data)
def Update(self, doc: c4d.documents.BaseDocument) -> None:
"""Updates the cache with the state of #doc.
Only takes selected objects into account in #doc.
Note:
This type does not ensure being only being fed with data from only one document. This
could be added by checking the UUID of the document itself, because a BaseDocument is
a C4DAtom instance too. UUID collisions for nodes in different documents are very
unlikely though. The cleaner way would be to store data per document.
"""
# Get all selected point objects in #doc and put their state into the cache.
selection: list[c4d.BaseObject] = [item for item in
doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN) if item.CheckType(c4d.Opoint)]
for item in selection:
self._update(item)
class SelectionStateDialog (c4d.gui.GeDialog):
"""Realizes a dialog which when opened, will track the changes of selection states of all
selected point objects in a scene.
When realized as a plugin, this would likely be replaced with a MessageData plugin in Python.
The code would however largely remain the same.
"""
# An instance of a SelectionStateCache attached to the class, as attaching it to a class
# instance itself does not make too much sense.
SELECTION_CACHE: SelectionStateCache = SelectionStateCache()
def CreateLayout(self) -> bool:
"""Called by Cinema 4D to let a dialog populate its GUI.
"""
self.SetTitle("SelectionStateDialog")
self.GroupBorderSpace(5, 5, 5, 5) # Sets the spacing of the implicit outmost dialog group.
self.AddStaticText(1000, c4d.BFH_SCALEFIT, name="Does not have any GUI.")
return True
def CoreMessage(self, mid: int, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D to convey core events.
Is here being used to react to state changes in the scene.
"""
if mid == c4d.EVMSG_CHANGE:
# Get the active document and feed it into the cache attached to the class of this
# dialog. Getting the active document is not necessary in a Script Manager script, but
# would be in a plugin.
cache: SelectionStateCache = SelectionStateDialog.SELECTION_CACHE
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
cache.Update(doc)
# Print the new state for each selected point object in the scene when cache has changed.
if not cache.IsDirty:
return super().CoreMessage(mid, msg)
print("Temporally ordered selection states:")
for node in [item for item in
doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
if item.CheckType(c4d.Opoint)]:
print (f"{node.GetName()}: {cache.GetTemporallyOrderedSelection(node)}")
return super().CoreMessage(mid, msg)
if __name__ == "__main__":
dlg: SelectionStateDialog = SelectionStateDialog()
dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=300, defaulth=50)