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

    How to draw on spline points from spline returned by Object Data

    Cinema 4D SDK
    2
    4
    635
    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.
    • H
      HerrMay
      last edited by

      Hi guys,

      following the py-osffset_y_spline example from the sdk examples I wonder how one would draw e.g. circles on the spline points of the returned spline object.

      Can someone help me here. 🤔

      Cheers,
      Sebastian

      """
      Copyright: MAXON Computer GmbH
      Author: Riccardo Gigante, Maxime Adam
      
      Description:
          - Retrieves the first child object and offset all its points on the y-axis by a specific value. Tangents are unaffected.
          - Demonstrates a Spline Generator that requires Input Spline and Outputs a valid Spline everywhere in Cinema 4D.
      
      Note:
          - SplineInputGeneratorHelper is a class holding utility functions
              SplineInputGeneratorHelper.OffsetSpline is the function responsible for moving points.
      
      Class/method highlighted:
          - c4d.SplineObject / c4d.LineObject / c4d.PointObject
          - c4d.plugins.ObjectData
          - ObjectData.Init()
          - ObjectData.GetDimension()
          - ObjectData.CheckDirty()
          - ObjectData.GetVirtualObjects()
          - ObjectData.GetContour()
      
      """
      import c4d
      
      PLUGIN_ID = 98989801
      
      
      class SplineInputGeneratorHelper(object):
      
          @staticmethod
          def FinalSpline(sourceSplineObj):
              """Retrieves the final (deformed) representation of the spline.
      
              Args:
                  sourceSplineObj (c4d.BaseObject or c4d.SplineObject or LineObject): A c4d.BaseObject that can be represented as a Spline.
      
              Returns:
                  c4d.SplineObject: The final Spline/Line Object, SplineObject should be returned when it's possible
              """
              if sourceSplineObj is None:
                  raise TypeError("Expect a spline object got {0}".format(sourceSplineObj.__class__.__name__))
      
              if sourceSplineObj.IsInstanceOf(c4d.Onull):
                  return None
      
              # Checks if sourceSplineObj can be treated as a spline
              if not sourceSplineObj.IsInstanceOf(c4d.Oline) and not sourceSplineObj.GetInfo() & c4d.OBJECT_ISSPLINE:
                  raise TypeError("Expect an object that can be treated as spline object.")
      
              # If there is a Deformed cache, retrieves it, but it seems it's never the case
              deformedCache = sourceSplineObj.GetDeformCache()
              if deformedCache is not None:
                  sourceSplineObj = deformedCache
      
              # Returns the LineObject if it's a LineObject
              if sourceSplineObj.IsInstanceOf(c4d.Oline):
                  return sourceSplineObj
      
              # Retrieves the SplineObject
              realSpline = sourceSplineObj.GetRealSpline()
              if realSpline is None:
                  raise RuntimeError("Failed to retrieve the real c4d.SplineObject from {0}".format(sourceSplineObj))
      
              return realSpline
      
          @staticmethod
          def OffsetSpline(inputSpline, offsetValue):
              """Performs the Y-Offset of the spline. 
      
              Take care the inputSpline can be sometime a LineObject or a SplineObject depending of the context (called from GVO or GetContour).
      
              Args:
                  inputSpline (Union[c4d.LineObject, c4d.SplineObject]): The original LineObject or SplineObject
                  offsetValue (float): The amount to offset Y parameter
      
              Returns:
                  Union[c4d.LineObject, c4d.SplineObject]: A new Line/Spline instance
              """
              if inputSpline is None:
                  raise TypeError("Expect a SplineObject got {0}".format(inputSpline.__class__.__name__))
      
              # Checks if the the input object is a SplineObject or a LineObject
              if not inputSpline.IsInstanceOf(c4d.Ospline) and not inputSpline.IsInstanceOf(c4d.Oline):
                  raise TypeError("Expect a SplineObject or a LineObject got {0}".format(inputSpline.__class__.__name__))
      
              # Retrieves a clones of the Splines/LineObject, so tangents and all parameters are kept.
              resSpline = inputSpline.GetClone()
              if resSpline is None:
                  raise MemoryError("Failed to create a new Spline Object.")
      
              # Retrieves all points position of the source object
              allPts = inputSpline.GetAllPoints()
              if not allPts:
                  return
      
              # Adds the offsetValue in Y for each point (this is done only in memory)
              allPtsOffsets = [c4d.Vector(pt.x, pt.y + offsetValue, pt.z) for pt in allPts]
      
              # Sets all points of the resSpline from the list previously calculated.
              resSpline.SetAllPoints(allPtsOffsets)
      
              # Notifies about the generator update
              resSpline.Message(c4d.MSG_UPDATE)
      
              # Returns the computed spline
              return resSpline
      
          @staticmethod
          def GetCloneSpline(op):
              """Emulates the GetHierarchyClone in the GetContour by using the SendModelingCommand.
      
              Args:
                  op (c4d.BaseObject): The Object to clone and retrieve the current state (take care the whole hierarchy is join into one object.
      
              Returns:
                  Union[c4d.BaseObject, None]: The merged object or None, if the retrieved object is not a Spline.
              """
              # Copies the original object
              childSpline = op.GetClone(c4d.COPYFLAGS_NO_ANIMATION)
              if childSpline is None:
                  raise RuntimeError("Failed to copy the child spline.")
      
              doc = c4d.documents.BaseDocument()
              if not doc:
                  raise RuntimeError("Failed to Create a Doc")
      
              doc.InsertObject(childSpline)
      
              # Performs a Current State to Object
              resultCSTO = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT, list=[childSpline], doc=doc)
              if not isinstance(resultCSTO, list) or not resultCSTO:
                  raise RuntimeError("Failed to perform MCOMMAND_CURRENTSTATETOOBJECT.")
      
              childSpline = resultCSTO[0]
      
              # If the results is a Null, performs a Join command to retrieve only one object.
              if childSpline.CheckType(c4d.Onull):
                  resultJoin = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_JOIN, list=[childSpline], doc=doc)
                  if not isinstance(resultJoin, list) or not resultJoin:
                      raise RuntimeError("Failed to perform MCOMMAND_JOIN.")
      
                  childSpline = resultJoin[0]
      
              if childSpline is None:
                  raise RuntimeError("Failed to retrieve cached spline.")
      
              # Checks if childSpline can be interpreted as a Spline.
              if not childSpline.GetInfo() & c4d.OBJECT_ISSPLINE and not childSpline.IsInstanceOf(c4d.Ospline) and not childSpline.IsInstanceOf(c4d.Oline):
                  return None
      
              return childSpline
      
          @staticmethod
          def HierarchyIterator(obj):
              """A Generator to iterate over the Hierarchy.
      
              Args:
                  obj (c4d.BaseObject): The starting object of the generator (will be the first result)
      
              Returns:
                  c4d.BaseObject: All objects under and next of the `obj`
              """
              while obj:
                  yield obj
                  for opChild in SplineInputGeneratorHelper.HierarchyIterator(obj.GetDown()):
                      yield opChild
                  obj = obj.GetNext()
      
      
      class OffsetYSpline(c4d.plugins.ObjectData):
      
          _childContourDirty = 0  # type: int
          _childGVODirty = -1  # type: int
      
          def Init(self, op):
              if op is None:
                  raise RuntimeError("Failed to retrieve op.")
      
              self.InitAttr(op, float, [c4d.PY_OFFSETYSPLINE_OFFSET])
              op[c4d.PY_OFFSETYSPLINE_OFFSET] = 100.0
      
              # Defines members variable to store the dirty state of Children Spline
              self._childContourDirty = 0
              self._childGVODirty = -1
      
              return True
      
          def Draw(self, op, drawpass, bd, bh):
              # How to go about drawing e.g. circles on every spline point of the returned spline object?
      
          def GetDimension(self, op, mp, rad):
              """This Method is called automatically when Cinema 4D try to retrieve the boundaries of the object.
      
              Args:
                  op (c4d.BaseObject): The Python Generator base object.
                  mp (c4d.Vector): Assign the center point of the bounding box to this vector.
                  rad (float): Assign the XYZ bounding box radius to this vector.
              """
              if op is None:
                  raise RuntimeError("Failed to retrieve op.")
      
              # Initializes default values
              mp, rad = c4d.Vector(), c4d.Vector()
      
              # If there is no child, that means the generator output nothing, so an empty size
              if op.GetDown() is None:
                  return
      
              # Assigns value as the child object
              rad = op.GetDown().GetRad()
              mp = op.GetMg().off
      
              # Offsets the bounding box by the Y offset the generator deliver
              mp.y = op.GetMg().off.y + op[c4d.PY_OFFSETYSPLINE_OFFSET]
      
          def CheckDirty(self, op, doc):
              """This Method is called automatically when Cinema 4D ask the object is dirty,
              something changed so a new computation of the generator is needed.
      
              In reality this is only useful for GetContour, GetVirtualObjects is automatically handled by Cinema 4D,
              But since the spline returned by GetContour is cached by Cinema 4D, you have to use CheckDirty
              To define when a new call of GetContour is needed. Moreover CheckDirty is only called in some special event,
              e.g. the Python Spline Generator is under another Python Spline generator.
      
              Args:
                  op (c4d.BaseObject): The Python Generator c4d.BaseObject.
                  doc (c4d.documents.BaseDocument): The document containing the plugin object.
              """
              if op is None or doc is None:
                  raise RuntimeError("Failed to retrieve op or doc.")
      
              # Retrieves the First Child
              child = op.GetDown()
              if child is None:
                  self._childContourDirty = -1
                  op.SetDirty(c4d.DIRTYFLAGS_DATA)
                  return
      
              # Retrieves the dirty count of the first child if there is a spline, otherwise set it to -1
              childDirty = child.GetDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX | c4d.DIRTYFLAGS_CACHE)
      
              # Checks if the dirty changed, if this is the case set op as dirty (it will force GetContour to be called)
              if childDirty != self._childContourDirty:
                  self._childContourDirty = childDirty
                  op.SetDirty(c4d.DIRTYFLAGS_DATA)
      
          def GetVirtualObjects(self, op, hh):
              """This method is called automatically when Cinema 4D ask for the cache of an object. 
      
              This is also the place where objects have to be marked as input object by touching them (destroy their cache in order to disable them in Viewport)
      
              Args:
                  op (c4d.BaseObject): The Python Generator c4d.BaseObject.
                  hh (c4d.HierarchyHelp): The helper object.
      
              Returns:
                  Union[c4d.LineObject, c4d.SplineObject]: The represented Spline.
              """
              if op is None or hh is None:
                  raise RuntimeError("Failed to retrieve op or hh.")
      
              # Retrieves the first enabled child
              child = op.GetDown()
              if child is None:
                  self._childGVODirty = -1
                  return
      
              # Touches all others children sine we don't want to have them later
              for obj in SplineInputGeneratorHelper.HierarchyIterator(op.GetDown().GetNext()):
                  obj.Touch()
      
              # Retrieves the Clones, then mark them as read
              resGHC = op.GetHierarchyClone(hh, child, c4d.HIERARCHYCLONEFLAGS_ASSPLINE)
              if resGHC is None:
                  resGHC = op.GetAndCheckHierarchyClone(hh, child, c4d.HIERARCHYCLONEFLAGS_ASSPLINE, False)
              if resGHC is None:
                  return None
      
              # Retrieves results
              opDirty = resGHC["dirty"]
              childSpline = resGHC["clone"]
              if childSpline is None:
                  return None
      
              # Checks if childSpline can be interpreted as a Spline.
              if not childSpline.GetInfo() & c4d.OBJECT_ISSPLINE and not childSpline.IsInstanceOf(c4d.Ospline) and not childSpline.IsInstanceOf(c4d.Oline):
                  return None
      
              # Checks if the dirty of the child changed
              opDirty |= op.IsDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX)
              childDirty = child.GetDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX | c4d.DIRTYFLAGS_CACHE)
      
              # If the dirty count didn't change, return the cache
              if childDirty == self._childGVODirty and not opDirty:
                  self._childGVODirty = child.GetDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX | c4d.DIRTYFLAGS_CACHE)
                  return op.GetCache()
      
              # Retrieves the deformed Spline/LineObject (most of the time it's a LineObject)
              deformedSpline = SplineInputGeneratorHelper.FinalSpline(childSpline)
              if deformedSpline is None:
                  return c4d.BaseObject(c4d.Onull)
      
              # Performs operation on the spline
              resSpline = SplineInputGeneratorHelper.OffsetSpline(deformedSpline, op[c4d.PY_OFFSETYSPLINE_OFFSET])
      
              # Returns the modified spline
              return resSpline
      
          def GetContour(self, op, doc, lod, bt):
              """This method is called automatically when Cinema 4D ask for a SplineObject, it's not called every time,
              only in some conditions like nested Spline Generator.
      
              Args:
                  op (c4d.BaseObject): The Python Generator c4d.BaseObject.
                  doc (c4d.documents.BaseDocument): The document containing the plugin object.
                  lod (int): The level of detail.
                  bt (c4d.threading.BaseThread): The executing thread.
      
              Returns:
                  The SplineObject representing the final Spline.
              """
              if op is None or doc is None:
                  raise RuntimeError("Failed to retrieve op or doc.")
      
              # Retrieves the first spline and set dirtyCount to 0 if the spline does not exists.
              childSpline = op.GetDown()
              if childSpline is None:
                  self._childContourDirty = 0
                  return None
      
              # Retrieves a Clone working spline.
              childSplineClone = SplineInputGeneratorHelper.GetCloneSpline(childSpline)
              if childSplineClone is None:
                  return None
      
              # Retrieves the deformed Spline/LineObject
              deformedSpline = SplineInputGeneratorHelper.FinalSpline(childSplineClone)
              if deformedSpline is None:
                  return None
      
              # Performs operation on the spline
              resSpline = SplineInputGeneratorHelper.OffsetSpline(deformedSpline, op[c4d.PY_OFFSETYSPLINE_OFFSET])
      
              # Updates dirtyCount for the child spline
              self._childContourDirty = childSpline.GetDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX | c4d.DIRTYFLAGS_CACHE)
      
              return resSpline
      
      
      if __name__ == "__main__":
          c4d.plugins.RegisterObjectPlugin(id=PLUGIN_ID,
                                           str="Py-OffsetY",
                                           g=OffsetYSpline,
                                           description="py_offsets_y_spline",
                                           icon=None,
                                           info=c4d.OBJECT_GENERATOR | c4d.OBJECT_INPUT | c4d.OBJECT_ISSPLINE)
      
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @HerrMay
        last edited by ferdinand

        Hello @HerrMay,

        Thank you for reaching out to us. The scope of your question is a bit broad, I would invite you to have a look at our multiple Draw function examples on GitHub.

        I am also not sure if you indeed want to draw circles, or if points classify for you as circles. When you want to draw indeed circles, you must decide if you want to draw them in screen space, i.e., use BaseDraw.DrawCircle2D to draw circles that always face the screen; or use BaseDraw.DrawCircle to draw circles that rotate with the spline in world space. For the latter you would then have to decide how to construct/define the orientation of each circle.

        Find a simple example for drawing points below.

        Cheers,
        Ferdinand

        Result:

        b2db4e9f-b1fb-4486-abfe-b40bbe87efa9-image.png

        Code:

            def Draw(self, op, drawpass, bd, bh) -> int:
                """Called by Cinema 4D to let a plugin draw into the given view port.
                """
                # Only draw into the object draw pass and ignore for example the highlight pass. The object
                # draw pass is the only draw pass to draw into for most drawing operations.
                if drawpass != c4d.DRAWPASS_OBJECT:
                    return c4d.DRAWRESULT_OK
        
                # We could also add more checks, as for example only drawing into view ports that have the
                # "Perspective" projection.
                if bd[c4d.BASEDRAW_DATA_PROJECTION] != c4d.BASEDRAW_PROJECTION_PERSPECTIVE:
                    return c4d.DRAWRESULT_OK
        
                # Get the cache of the object representing the plugin in the object manager, i.e., more or
                # less what we returned in GetContour, Cinema 4D has however exhausted the cache for us, and
                # the cache is not a SplineObject but a LineObject (i.e, the cache of a SplineObject).
                cache: c4d.LineObject = op.GetDeformCache() or op.GetCache()
                if not isinstance(cache, c4d.LineObject):
                    return c4d.DRAWRESULT_OK
        
                # Set the drawing space to the spline object, so that we can draw points of that object
                # without any further transformations. We also draw over almost everything else with a 
                # z-offset of five.
                bd.SetMatrix_Matrix(op, op.GetMg(), zoffset=5)
        
                # Get the points of the line object and construct a gradient of colors for the points 
                # determined by index. BaseDraw.DrawPoints is a bit weird as it takes a list[float] for
                # the colors of the points and not a list[c4d.Vector].
                count: int = cache.GetPointCount()
                points: list[c4d.Vector] = cache.GetAllPoints()
                colors: list[float] = []
                for i in range(count):
                    f: float = i/count
                    colors += [f, f * .5, 0]
        
                # Set the point size to draw with and draw the points.
                bd.SetPointSize(10)
                bd.DrawPoints(points, colors, 3)
        
                return c4d.DRAWRESULT_OK
        

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • H
          HerrMay
          last edited by

          Hello @ferdinand,

          sorry for being a bit ambigous here. Drawing points is actually exactly what I wanted. 😊
          As always thanks for your time and effort.

          Funny enough DrawPoints seems to internally use the "same points" to draw, one gets to see when one edits the points of a polygon object. 😄 In S26 the points are round (thats probably why I was thinking of circles) while in R20 they are squares. No question here, just an observation.

          What is actually weird though is that the color gradient you used here to color the points doesn't work in R20. The points are simply drawn black there. Possibly a Python 2/3 issue caused by integer/float division differences? 🤷

          Allow one follow up question. How can I draw only the control points of the spline? In your example you use a circle primitive which has - when made editable - exactly four points making up the shape via tangents. Where as the drawn points using the intermediate points of the circle as well.

          Cheers,
          Sebastian

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

            Hello @HerrMay,

            Funny enough DrawPoints seems to internally use the "same points" to draw, one gets to see when one edits the points of a polygon object. 😄 In S26 the points are round (thats probably why I was thinking of circles) while in R20 they are squares. No question here, just an observation.

            As you can see, in the script I retrieve the cache of the object we are going to draw.

            # Get the cache of the object representing the plugin in the object manager, i.e., more or
            # less what we returned in GetContour, Cinema 4D has however exhausted the cache for us, and
            # the cache is not a SplineObject but a LineObject (i.e, the cache of a SplineObject).
            cache: c4d.LineObject = op.GetDeformCache() or op.GetCache()
            if not isinstance(cache, c4d.LineObject):
                return c4d.DRAWRESULT_O
            

            This not only enables us to deal with splines affected by a deformer correctly, but also and more importantly with spline generators as for example the Circle spline generator.

            Such generators do not have any control points one could retrieve, their cache will always contain LineObject instances and not SplineObject instances (when they have been implemented correctly). This is also what I was referring to in my comment, because OffsetYSpline returns a SplineObject as its cache in GetContour, but Cinema 4D will 'exhaust' that cache for us by returning the cache of the cache, the LineObject.

            A LineObject represents a SplineObject over its current interpolation settings.

            What is actually weird though is that the color gradient you used here to color the points doesn't work in R20. The points are simply drawn black there. Possibly a Python 2/3 issue caused by integer/float division differences? 🤷

            Yes, this is Python 3 code. In Python 2 integers divide by default as integers. Simply wrap one of the numbers into a float, e.g., f: float = float(i)/float(count).

            Allow one follow up question. How can I draw only the control points of the spline? In your example you use a circle primitive which has - when made editable - exactly four points making up the shape via tangents. Where as the drawn points using the intermediate points of the circle as well.

            You can use BaseObject.GetRealSpline to get the underlying editable spline object for a spline. So, when you would replace the section quoted above with this:

            cache: c4d.SplineObject = op.GetRealSpline()
            if not isinstance(cache, c4d.SplineObject):
                return c4d.DRAWRESULT_OK
            

            You would now draw the control points of the SplineObject representing the spline generator rather than the vertices of its underlying LineObject. The problem in this case is that the plugin you have chosen is calling 'Current State to Object' (CSTO) on the input object which it is cloning and returning offset-ed (see line 130 in your code listing). CSTO is of course just replacing an object with its cache state. So, when you put a circle spline below the offset spline, it will still be drawn as in the LineObject cache version because the cache and the BaseObject.GetRealSpline representation are in this case identical.

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 0
            • First post
              Last post