Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Can I get the index of the last selected point among the selected points?

    Cinema 4D SDK
    python
    2
    5
    1.1k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • ymoonY
      ymoon
      last edited by ymoon

      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")
      
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @ymoon
        last edited by ferdinand

        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:
        input_usr_area.gif

        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)
        
        

        MAXON SDK Specialist
        developers.maxon.net

        ymoonY 1 Reply Last reply Reply Quote 1
        • ferdinandF ferdinand referenced this topic on
        • i_mazlovI i_mazlov referenced this topic on
        • ymoonY
          ymoon @ferdinand
          last edited by

          @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.

          2023-04-21_220537.png

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @ymoon
            last edited by

            Hey @ymoon,

            My apologies, I am not quite sure what happend there, but the h in defaulth was missing in my code listing. The code should run now, I updated the listing.

            Cheers,
            Ferdinands

            MAXON SDK Specialist
            developers.maxon.net

            ymoonY 1 Reply Last reply Reply Quote 0
            • ymoonY
              ymoon @ferdinand
              last edited by

              @ferdinand
              It Works. Thank You

              1 Reply Last reply Reply Quote 0
              • G Gaal Dornik referenced this topic on
              • First post
                Last post