Can I get the index of the last selected point among the selected points?
-
Can I get the index of the last selected point among the selected points?
Is this not possible with a python script(.py)?
Thanks.SDK c4d.BaseSelect
example code - always return in order.
1-5-4-2(wanna this) ---> 1-2-4-5 (not this)bs = op.GetPointS() sel = bs.GetAll(op.GetPointCount()) for index, selected in enumerate(sel): if not selected: continue print(f"Index {index} is selected")
-
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 aBaseSelect
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 toEVMSG_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,
FerdinandResult:
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://developers.maxon.net/forum/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)
- (Sort of recommended) Caching things yourself: Implement an entity which watches the scene selection state. You could use a
-
-
-
@ferdinand
As expected, it's too difficult. And some of the code doesn't seem to work properly. Could you please upload the scene. thank you It has been resolved. -
Hey @ymoon,
My apologies, I am not quite sure what happend there, but the
h
indefaulth
was missing in my code listing. The code should run now, I updated the listing.Cheers,
Ferdinands -
@ferdinand
It Works. Thank You -