Undo and Redo

Since 1.6

You can add support for the user to undo and redo SDK operations using an Project.undo() context manager. Simply place the SDK operations within the following with statement:

with project.undo():

Any SDK operations within the with block will be able to be undone and subsequently redone by the user via the (Undo) and (Redo) buttons (or Ctrl + Z and Ctrl + Y shortcuts) in the connected application.

Note:  Undo/redo support requires PointStudio 2023.1 or Vulcan GeologyCore 2023.2 or newer.

Basic undo and redo

The simplest use of the Project.undo() context manager is to support undo and redo after creating an object via the SDK. For example, the following script creates a tetrahedron-shaped surface:

from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import Surface, ObjectID


def create_tetrahedron_with_undo(project: Project, path: str) -> ObjectID[Surface]:
    """Create a tetrahedron which can be undone.


    Parameters
    ----------
    project
      Project to create the tetrahedron surface in.
    path
      The path to create the tetrahedron at. If there is already an object
      with this path the path will be postfixed with the smallest possible
      number to ensure that an existing object is not overwritten.
    """
    with project.undo():
        with project.new(path, Surface, overwrite=OverwriteMode.UNIQUE_NAME
                ) as tetrahedron:
            size = 3
            tetrahedron.points = [
                [-size, -size, -size], [-size, size, size],
                [size, -size, size], [size, size, -size]]
            tetrahedron.facets = [[0, 1, 2], [0, 2, 3], [0, 1, 3], [1, 2, 3]]
    return tetrahedron.id


if __name__ == "__main__":
    main_project = Project()
    _ = create_tetrahedron_with_undo(main_project, "surfaces/tetrahedron")

At first glance, the above script works the same as any other script. However, because the call to Project.new() is inside of the Project.undo() block, the creation of the tetrahedron can be undone by pressing the undo button in the connected application.

It is also possible to undo edits to objects made via Project.edit(). For example, the following script will colour the selected surface “sky blue”. A press of the undo button will return the surface’s colour to what it was before the script was run:

from mapteksdk.project import Project
from mapteksdk.data import Surface, ObjectID
from mapteksdk.operations import object_pick


def colour_surface_with_undo(project: Project, surface_id: ObjectID[Surface]):
    """Change the colour of a surface with undo.


    Parameters
    ----------
    project
      Project the surface is part of.
    surface_id
      Object ID of the surface to change the colour of.
    """
    with project.undo():
        with project.edit(surface_id, Surface) as surface:
            # This is "Sky blue"
            surface.facet_colours = [135, 206, 235, 255]


if __name__ == "__main__":
    with Project() as main_project:
        oid = object_pick(
            object_types=Surface,
            label="Pick a surface to colour sky blue"
        )
        colour_surface_with_undo(main_project, oid)

Multiple undo blocks

If a script contains multiple non-overlapping Project.undo() blocks, then it will take multiple presses of the undo button to fully undo the script. Each press of the undo button will undo the contents of a single Project.undo() block. For example, consider the following script that combines the previous two example scripts together:

from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import Surface, ObjectID


def create_tetrahedron_with_undo(project: Project, path: str) -> ObjectID[Surface]:
    """Create a tetrahedron which can be undone.


    Parameters
    ----------
    project
      Project to create the tetrahedron surface in.
    path
      The path to create the tetrahedron at. If there is already an object
      with this path the path will be postfixed with the smallest possible
      number to ensure that an existing object is not overwritten.
    """
    with project.undo():
        with project.new(path, Surface, overwrite=OverwriteMode.UNIQUE_NAME
                ) as tetrahedron:
            size = 3
            tetrahedron.points = [
                [-size, -size, -size], [-size, size, size],
                [size, -size, size], [size, size, -size]]
            tetrahedron.facets = [[0, 1, 2], [0, 2, 3], [0, 1, 3], [1, 2, 3]]
    return tetrahedron.id


def colour_surface_with_undo(project: Project, surface_id: ObjectID[Surface]):
    """Change the colour of a surface with undo.


    Parameters
    ----------
    project
      Project the surface is part of.
    surface_id
      Object ID of the surface to change the colour of.
    """
    with project.undo():
        with project.edit(surface_id, Surface) as surface:
            # This is "Sky blue"
            surface.facet_colours = [135, 206, 235, 255]


if __name__ == "__main__":
    main_project = Project()
    oid = create_tetrahedron_with_undo(main_project, "surfaces/tetrahedron")
    colour_surface_with_undo(main_project, oid)

The first undo click will undo the colouring of the surface. The second will undo the creation of the surface.

Allowing a script to be partially undone is useful for scripts where it makes sense to undo some but not all the changes.

Combining undo blocks

If a script contains multiple non-overlapping Project.undo() blocks, then it will take multiple presses of the undo button to fully undo the changes made by the script. Often, it is desirable to combine these multiple smaller Project.undo() blocks together such that a single press of the undo button will undo all of the changes at once. This can be achieved by placing the smaller Project.undo() blocks inside another Project.undo() block. For example, the script from the previous section can be modified to be undone in a single step by adding a Project.undo() block around the functions which can be undone:

from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import Surface, ObjectID


def create_tetrahedron_with_undo(project: Project, path: str) -> ObjectID[Surface]:
    """Create a tetrahedron which can be undone.


    Parameters
    ----------
    project
      Project to create the tetrahedron surface in.
    path
      The path to create the tetrahedron at. If there is already an object
      with this path the path will be postfixed with the smallest possible
      number to ensure that an existing object is not overwritten.
    """
    with project.undo():
        with project.new(path, Surface, overwrite=OverwriteMode.UNIQUE_NAME
                ) as tetrahedron:
            size = 3
            tetrahedron.points = [
                [-size, -size, -size], [-size, size, size],
                [size, -size, size], [size, size, -size]]
            tetrahedron.facets = [[0, 1, 2], [0, 2, 3], [0, 1, 3], [1, 2, 3]]
    return tetrahedron.id


def colour_surface_with_undo(project: Project, surface_id: ObjectID[Surface]):
    """Change the colour of a surface with undo.


    Parameters
    ----------
    project
      Project the surface is part of.
    surface_id
      Object ID of the surface to change the colour of.
    """
    with project.undo():
        with project.edit(surface_id, Surface) as surface:
            # This is "Sky blue"
            surface.facet_colours = [135, 206, 235, 255]


if __name__ == "__main__":
    main_project = Project()
    with main_project.undo():
      oid = create_tetrahedron_with_undo(main_project, "surfaces/tetrahedron")
      colour_surface_with_undo(main_project, oid)

When this script is run, both the creation of the surface and the editing of its colours will be undone with a single press of the undo button.

Useful Fragment

The following fragment is useful for writing scripts with undo / redo support:

"""Fragment demonstrating a good way to write scripts which support undo.


When using this fragment, you should avoid calling functions from the
operations module which edit the object, as they may cause odd undo / redo
behaviour.
"""
from mapteksdk.project import Project


def main(project: Project):
    """The main function for the script."""
    raise NotImplementedError(
        "The implementation of the script goes here."
    )


if __name__ == "__main__":
    with Project() as main_project:
        with main_project.undo():
            main(main_project)

This can be combined with the error handling fragment described in Output Mechanisms > Template script for message on error.

"""Fragment demonstrating a good way to write scripts which support undo.


This has also been expanded to use toast notifications and messages to report
errors to the user.


When using this fragment, you should avoid calling functions from the
operations module which edit the object, as they may cause odd undo / redo
behaviour.
"""
import os


from mapteksdk.operations import show_message, show_toast_notification, Severity
from mapteksdk.project import Project


SCRIPT_NAME = os.path.basename(__file__)


def main(project: Project):
    """The main function for the script."""
    raise NotImplementedError(
        "The implementation of the script goes here."
    )


if __name__ == "__main__":
    with Project() as main_project:
        try:
            with main_project.undo():
                main(main_project)
                show_toast_notification(SCRIPT_NAME, "Completed")
        except Exception as error:
            message = f"{type(error).__name__}: {str(error)}"
            show_message(SCRIPT_NAME, message, Severity.ERROR)
            raise

Limitations

The following limitations of the undo/redo facility apply:

  • Functions in the mapteksdk.operations module cannot be undone or redone.

  • You cannot call Project.delete() from within a Project.undo() block. Since this function permanently deletes an object, it is inherently an operation that cannot be undone. Use Project.recycle() instead to support undo.

  • You cannot undo changes made to Drillhole or DrillholeDatabase objects. Thus you cannot open such objects for editing within a Project.undo() block.

  • Calling functions from the operations module that edit objects (e.g. despike()) in a Project.undo() block may result in unexpected undo/redo behaviour.

  • If there is an unhandled exception inside of a Project.undo() block then any already saved changes will be undoable by pressing the undo button.

  • If multiple Project.undo() blocks are inside of another Project.undo() block, then they will be combined together so that they can all be undone with a single press of the undo button.

  • If a script makes some changes inside of a Project.undo() block and some outside, only those made within the Project.undo() block will be undoable. This may cause unexpected undo behaviour.