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

    Why do I have to hit Undo twice after performing SendModelingCommand 'Weld'?

    Cinema 4D SDK
    windows s26 python
    2
    3
    581
    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,

      so as the title suggests I observed some strange behaviour of the SMC Weld command in conjunction with BaseObject.GetModelingAxis().

      My test setup is as follows:
      I have a simple plane objects made editable. Then I select some vertices I want to weld together. I then move the Modeling Axis to the random position and execute the script down below.

      The first question I have is why can't I weld the points directly together at the position vector BaseObject.GetModelingAxis() provides me with?

      To circumvent this issue I came up with the workaround to first weld the point selection to its 'center' while afterwards move that point to the position of BaseObject.GetModelingAxis().

      The second question possibly arises from this workaround because while it works it seems that this, way the undo system gets screwed up. To undo the weld operation I have to hit undo twice. Why is that?

      I hope someone can help me here and I could explain my problem well enough. If not just let me know.

      Cheers,
      Sebastian

      from typing import Optional, Generator, Union
      import c4d
      
      doc: c4d.documents.BaseDocument  # The active document
      op: Optional[c4d.BaseObject]  # The active object, None if unselected
      
      
      def is_mesh(node: c4d.BaseObject) -> bool:
          """Return whether *node* is of type *c4d.PointObject*."""
      
          return isinstance(node, c4d.PointObject)
      
      
      def iter_selected_points(obj: c4d.BaseObject) -> Generator:
          """Yield selected points of *obj* along with their index."""
      
          points = obj.GetAllPoints()
          baseselect = obj.GetPointS()
      
          sel = baseselect.GetAll(obj.GetPointCount())
      
          for (index, selected), point in zip(enumerate(sel), points):
              if not selected:
                  continue
              yield index, point
      
      
      def smc_weld(obj: c4d.PointObject, doc: c4d.documents.BaseDocument) -> bool:
          """Perform modeling comannd 'Weld' for selected points of *obj*."""
      
          settings = c4d.BaseContainer()
          # Explicitly say we will not use point, so the center will be used
          settings[c4d.MDATA_WELD_TOPOINT] = False
      
          result = c4d.utils.SendModelingCommand(
              command=c4d.ID_MODELING_WELD_TOOL,
              list=[obj],
              mode=c4d.MODELINGCOMMANDMODE_POINTSELECTION,
              bc=settings,
              doc=doc
          )
      
          return result
      
      
      def smc_optimize(obj: c4d.PointObject, doc: c4d.documents.BaseDocument, tolerance: float = 0.001) -> bool:
          """Perform modeling comannd 'Optimze' for all points of *obj*."""
      
          settings = c4d.BaseContainer()
          settings[c4d.MDATA_OPTIMIZE_TOLERANCE] = tolerance
          settings[c4d.MDATA_OPTIMIZE_POINTS] = True
          settings[c4d.MDATA_OPTIMIZE_POLYGONS] = True
          settings[c4d.MDATA_OPTIMIZE_UNUSEDPOINTS] = True
      
          result = c4d.utils.SendModelingCommand(
              command=c4d.MCOMMAND_OPTIMIZE,
              list=[obj],
              mode=c4d.MODELINGCOMMANDMODE_ALL,
              bc=settings,
              doc=doc
          )
      
          return result
      
      
      def main() -> None:
          # Called when the plugin is selected by the user. Similar to CommandData.Execute.
      
          c4d.StopAllThreads()
      
          doc = c4d.documents.GetActiveDocument()
          doc.StartUndo()
      
          obj = doc.GetActiveObject()
      
          if not obj:
              return
      
          doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj)
          position = obj.GetModelingAxis(doc).off
      
          if not smc_weld(obj, doc):
              msg = f"Could not execute Modeling Command 'Weld'."
              raise RuntimeError(msg)
      
          index = list(dict(iter_selected_points(obj)).keys())[0]
          obj.SetPoint(index, position)
          obj.Message(c4d.MSG_UPDATE)
      
          if not smc_optimize(obj, doc):
              msg = f"Could not execute Modeling Command 'Optimize'."
              raise RuntimeError(msg)
      
          doc.EndUndo()
          c4d.EventAdd()
      
      
      def state():
          # Defines the state of the command in a menu. Similar to CommandData.GetState.
      
          doc = c4d.documents.GetActiveDocument()
          obj = doc.GetActiveObject()
      
          if not obj:
              return False
      
          return c4d.CMD_ENABLED
      
      if __name__ == '__main__':
          main()
      
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @HerrMay
        last edited by ferdinand

        Hey @HerrMay,

        Thank you for reaching out to us. While I understand that it can be hard to follow this rule when you are in the middle of things, I must point out that you should limit the scope (i.e., number of questions) of your topics as they can become otherwise hard to answer.

        1. Most of your setup here is irrelevant for the problem at hand, the sole culprit seems to be the weld command.
        2. The weld command seems to be bugged (tested in S26.1 and 2024.1.0):
          • Some of its parameters behave oddly (e.g., MDATA_WELD_POINT).
          • The command seems to have problems with undos in multiple contexts, ranging from adding multiple undos, to completely breaking the undo stack.
          • As always it is a bit intransparent which of the tool/command parameters are still supported by the new modeling kernel.

        Given that the problem does exist in S26 and 2024 (and probably everything in between) and only for the weld tool, indicates that this problem likely originates in the modeling kernel and is not caused by the Python layer and its GIL. I have forwarded the problem to one of our modeling developers to have a look.

        But please understand that we cannot fix things for S26 retroactively. To answer a few of your questions:

        1. The tool does offer more control where to weld to, one is MDATA_WELD_POINTINDEX and the other is MDATA_WELD_POINT, although the latter is behaving weirdly.
        2. The undo problems are likely a bug.

        What can I do?

        I am afraid, not much from the Python side. The only thing I see here is a brute force approach. Just copy your weld object to a temporary document, do all computations there, and then copy the result back (wrapped in one undo). But you then of course run in the usual dependencies problem: Which entities must be copied into the temporary document? Are there dependencies such as deformers and fields; which when omitted will change the result?

        When you want to know more about operating in temporary documents with SMC, you should have a look at our smc_extrude_s26.py example.

        I will update this thread once we have conclude what is up with the weld tool (there is also a chance that you and I are simply misusing the tool, that there is magic order of operations/arguments one must follow).

        Code:

        from typing import Optional
        import c4d
        
        doc: c4d.documents.BaseDocument  # The active document
        op: Optional[c4d.BaseObject]  # The active object, None if unselected
        
        def main() -> None:
            """
            """
            if not op:
                return
        
            bc: c4d.BaseContainer = c4d.BaseContainer()
            # When set, use the point index to weld to.
            bc[c4d.MDATA_WELD_TOPOINT] = True
            # The point to weld to (does not have to be a selected point).
            bc[c4d.MDATA_WELD_POINTINDEX] = 0
            bc[c4d.MDATA_WELD_OBJECTINDEX] = op # ???
            bc[c4d.MDATA_WELD_POINT] = c4d.Vector(0) # Behaves weirdly.
        
            if not c4d.utils.SendModelingCommand(
                c4d.ID_MODELING_WELD_TOOL, [op], c4d.MODELINGCOMMANDMODE_POINTSELECTION, bc, doc):
                raise RuntimeError()
        
            c4d.EventAdd()
        
        if __name__ == '__main__':
            main()
        

        MAXON SDK Specialist
        developers.maxon.net

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

          Hi @ferdinand,

          ah okay I see. Some part of me already suspected it could be a bug. Thanks for taking the time to look into it. 🙂

          Regarding the scope of question. You are absolutely right. And while I understand that it can make answering branching questions hard, it is sometimes not that easy to split things up. But all good, no offense taken. 🙂

          One last thing, I know you guys are not here to develop solutions or come up with algorithms. Still, I was wondering if a possible solution could be to move the selected points to a custom vector position and afterwards simply send an "Optimize" modeling command?

          Cheers,
          Sebastian

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