SOLVED Merge Undos for the BeginTransaction() method?

Hi,

Is it possible to merge the undos for the BeginTransaction method?
Currently, if I create 100 materials with that method included, I have to undo it 100 times before I get back to the initial state.

I can confirm its the BeginTransaction issue because if I remove it I can undo once.

You can see the code below for illustration (only three materials):

import c4d

def main():

    mat_list = ['red', 'blue', 'green']
    
    doc.StartUndo()
    for mat_name in mat_list:
        new_mat = c4d.BaseMaterial(c4d.Mmaterial)
        nodeMaterial = new_mat.GetNodeMaterialReference()
        nodeMaterial.AddGraph("com.redshift3d.redshift4c4d.class.nodespace")
        doc.InsertMaterial(new_mat)
        new_mat.SetName(mat_name)
        doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, new_mat)

        nodeMaterial = new_mat.GetNodeMaterialReference()

        nodespaceId = c4d.GetActiveNodeSpaceId()
        nimbusRef = new_mat.GetNimbusRef(nodespaceId)
        graph = nimbusRef.GetGraph()

        with graph.BeginTransaction() as transaction:
            transaction.Commit()


    doc.EndUndo()

if __name__ == '__main__':
    main()

I'm guessing GraphModelInterface.StartNewChangeList() can be the solution. But I'm not sure how to apply it.

Hello @bentraje,

Thank you for reaching out to us. You must pass a settings dictionary to your transaction, telling it that you do not want a separate undo step for the transaction.

# Define a settings dictionary for the graph transactions. This one says, "no undo steps please".
settings: maxon.DataDictionaryInterface = maxon.DataDictionary()
settings.Set(maxon.nodes.UndoMode, maxon.nodes.UNDO_MODE.NONE)

and then later use that in your transaction(s):

# Start a transaction using our transaction settings.
with graph.BeginTransaction(settings) as transaction:
    diffuseIPort.SetDefaultValue(diffuseColor) # Set the diffuse color of the material.
    transaction. Commit()

Your code is also a bit lengthy in some parts. When you add a graph yourself, .AddGraph will already return the maxon.GraphModelRef for that graph. No need to go over the active space and the nimbus graph handler. Also - you first add the RS node space graph to a material, but then retrieve the graph for the active node space. Which at least somehow implies that you assume them to be always the same. But that is only true when RS is the active render engine. Nothing prevents you from adding and modifying material node graphs for node spaces which are not the active render engine (as long as you have the render engine installed and therefore the node space registered).

Find an example below.

Cheers,
Ferdinand

Result:
rs_mat_undo.gif

Code:

"""Demonstrates how to mute undo steps for graph transactions.

When run as a Script Manager script, #n Redshift node materials will be created, with each of
their diffuse color being set to the color #c. The creation of all materials as well as their
graph transactions will be wrapped into a singular undo. I.e., the modifications applied by running
the script once can be reversed with a singular [CTRL/CMD + Z].

The number of materials, their colors, and their names depends on #MATERIAL_DATA as defined below.
"""

import typing

import c4d
import maxon

__version__ = "2023.0.0"

doc: c4d.documents.BaseDocument # The active document.

# Defines materials as dictionaries of names and diffuse colors.
MATERIAL_DATA: list[dict[str, typing.Any]] = [
    {"name": "red", "diffuse_color": maxon.Color64(1, 0, 0)},
    {"name": "green", "diffuse_color": maxon.Color64(0, 1, 0)},
    {"name": "blue", "diffuse_color": maxon.Color64(0, 0, 1)}
]

# The Redshift node space ID.
ID_RS_NODESPACE: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")


def main():
    """Runs the example.
    """
    # Not really necessary to do, but since you implied tying the script to the active space being
    # Redshift, I did so too.
    if c4d.GetActiveNodeSpaceId() != ID_RS_NODESPACE:
        print ("Error: Active node space is not Redshift.")
        return

    # Define a settings dictionary for the graph transactions. This one says, "no undo steps please".
    settings: maxon.DataDictionaryInterface = maxon.DataDictionary()
    settings.Set(maxon.nodes.UndoMode, maxon.nodes.UNDO_MODE.NONE)

    # Open an undo stack for the active document, since we do this outside of the loop, all three
    # materials will be wrapped into a single undo.
    if not doc.StartUndo():
        raise MemoryError(f"{doc = }")

    # Iterate over the abstract material data and get the name and diffuse color for each.
    for data in MATERIAL_DATA:
        name: str = data.get("name", None)
        diffuseColor: maxon.Color64 = data.get("diffuse_color", None)

        # Allocate a new material, get the node material, and add a RS node space graph.
        material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
        material.SetName(name)
        nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
        graph: maxon.GraphModelRef = nodeMaterial.AddGraph(ID_RS_NODESPACE)
        if graph.IsNullValue():
            raise RuntimeError(f"Could not allocate '{ID_RS_NODESPACE}' graph.")

        # Try to find the standard material node inside this graph.
        result: list[maxon.GraphNode] = []
        maxon.GraphModelHelper.FindNodesByAssetId(
            graph, "com.redshift3d.redshift4c4d.nodes.core.material", False, result)
        if len(result) < 1 or result[0].IsNullValue():
            raise RuntimeError(f"{graph} does not contain a standard material node.")

        # Get diffuse color input port of that node.
        materialNode: maxon.GraphNode = result[0]
        diffuseIPort: maxon.GraphNode = materialNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.material.diffuse_color")
        if diffuseIPort.IsNullValue():
            raise RuntimeError("Could not find diffuse color port.")

        # Start a transaction using our transaction settings.
        with graph.BeginTransaction(settings) as transaction:
            diffuseIPort.SetDefaultValue(diffuseColor) # Set the diffuse color of the material.
            transaction.Commit()
        
        # Nothing went horribly wrong, we can actually add the material.
        doc.InsertMaterial(material)
        doc.AddUndo(c4d.UNDOTYPE_NEW, material)

    # Close the undo and raise an update event.
    if not doc.EndUndo():
        raise RuntimeError(f"{doc = }")

    c4d.EventAdd()

if __name__ == '__main__':
    main()

Hi @ferdinand

Thanks for the illustration code and video.
Works as expected.
And also for the tips on improving my code.

Just one thing, where do I find the other settings that I can set for the BeginTransaction?
The documentation doesn't seem to list them (both in Python and C++ docs). Or maybe I'm just missing them out.

Hey @bentraje,

yeah, they are undocumented.

The problem with the maxon API is that Doxygen or other code lexers do not really understand all its semantic complexity. maxon::nodes::UndoMode is a maxon attribute and Doxgen has no clue what to do with them, it simply ingests them all as MAXON_ATTRIBUTE (just search for MAXON_ATTRIBUTE and it will yield them all in one giant 2500 items large blob).

Technically it is there in the C++ docs when you look at the maxon::nodes namespace. Many maxon attributes are also not documented in a practical sense apart from this technical problem, they have no short description.

80d43244-278b-4d10-890a-09b22b264dcb-image.png

In Python it is a bit better due to all the manual labor by Maxime (everyone say "thaaanks Maxime" 🙂 ). The entity is indexable there but also carries no short description because it was never provided by the devs. The values written to this attribute are documented in C++ and indexed in Python.

We are very much aware of this and other problems with the maxon API and working towards a solution. In this case I think this is the only attribute, which is meaningful to set for transactions, but I will ask the devs or look at the code on Monday to be sure.

Cheers,
Ferdinand

@ferdinand

Gotcha. Thanks for the confirmation.
Will close the thread now.

P.S. Thanks @maxime ! hehe

Hey @bentraje,

almost forgot this thread. So, I had a look and there are more transaction attributes. You can find them under the namespace TransactionProperties (C++) and TransactionProperties (Python).

Most of them are, however, not too useful for public users. Meaningful are only:

  • DisableNotifications: Does what its name implies, it mutes observables. Muted is only maxon::GraphModelInterface::ObservableTransactionCommitted as far as I can see. Since observables are an unknown concept for Python users (as of 2023.1), this does not really apply to Python.
  • DoNotChangeAMMode: Suppresses a new node selection being propagated to the Attribute Manager. I.e., when you have a transaction which selects nodes or ports, and you set the attribute, committing this transaction will not change the AM mode and display the tristate of your new node selection.

The other three are more of an internal nature, and I do not see a good public use case for them.

Cheers,
Ferdinand

@ferdinand

gotcha. thanks for clarification and adding links.