Scripts to Help With Copy/Paste History

I’m doing a lot of copy/paste history and getting annoyed that something in the history chain wasn’t selectable. I’m rusty with python, but ChatGPT helped me out a bit. It got a few things wrong, like not knowing about scriptcontext.sticky[ ... ] (used doc user text), but all in all, it was actually helpful.

There are four scripts:

  • - turn on/unlock all layers, then _Show/_Unlock. It saves the existing state.
  • - Pick what you want to copy, and it will find all its parents and isolate them. You can manually hide things you don’t want to be included in the next step.
  • - Copies it to the clipboard, hides the original, pastes, and selects objs without history. This lets you move the pasted stuff to a new location.
    • You also have the option to change layer names. This lets you copy something from layer X and paste to layer Y.
  • - Restores the original selectability of things before you ran

First, there is a shared ‘’ to share between all the scripts:

import rhinoscriptsyntax as rs
import scriptcontext as sc

layerStatesKey = "layerState"
hiddenObjsKey = "hiddenObjs"
lockedObjsKey = "lockedObjs"

class LayerState:
    def __init__(self, layerId, visible, locked):
        self.layerId = layerId
        self.visible = visible
        self.locked = locked

class LayerName:
    def __init__(self, layerId, oldName, newName):
        self.layerId = layerId
        self.oldName = oldName
        self.newName = newName

def removeLayerParents(layerName):
    nameParts = layerName.split("::")
    return nameParts[-1]

def renameLayer(oldName, newName):
    modNewName = removeLayerParents(newName)
    rs.RenameLayer(oldName, modNewName)
1 Like

Surprisingly, Rhino doesn’t have a command to make everything in a file selectable (turn on/unlock all layers, then _Show/_Unlock all objects), but since I had to make it, I got to add a way to preserve the current state so you can undo it later.

Thanks to @stevebaer for _TestDumpSvgs in the latest WIP. I am upgrading by icon game.

v8 SVG: AllSelectable
v7 PNG: AllSelectable

import rhinoscriptsyntax as rs
import scriptcontext as sc
import eLib as e

def AllSelectable():
    # always skip turning on or off specific layers from a template file
    layersToSkip = [
        '1ced3d60-c6a7-48b5-81f1-4daef190945f', # Layout::MM::Grandpappy
        '67bfd040-c5c5-4493-9b0e-8061b7d8c488' # 0

    # get a list of all layers in the document
    layerIDs = rs.LayerIds()
    # create an empty list to store the layer information
    layerStates = []

    # loop through each layer and get its visibility and lock state
    for layerID in layerIDs:
        if str(layerID) not in layersToSkip:
            # get the layer's visibility and lock state
            wasVisible = rs.LayerVisible(layerID, True, False)
            wasLocked = rs.LayerLocked(layerID, False)
            state = e.LayerState(layerID, wasVisible, wasLocked)
            # add the dictionary to the list
            print("Skipping " + rs.LayerName(layerID))

    objects = rs.AllObjects()

    # create an empty list to store the hidden objects
    hiddenObjs = []
    lockedObjs = []

    # loop through each object and get its visibility
    for obj in objects:
        # get the object's current visibility
        if rs.IsObjectHidden(obj):
        if rs.IsObjectLocked(obj):

    print("Showed " + str(len(hiddenObjs)) + " objects.")
    print("Unlocked " + str(len(lockedObjs)) + " objects.")

    # save the hidden objects list to the Rhino script context's sticky dictionary
    sc.sticky[e.hiddenObjsKey] = hiddenObjs
    sc.sticky[e.lockedObjsKey] = lockedObjs
    sc.sticky[e.layerStatesKey] = layerStates

if (__name__ == "__main__"):

 # print(str(layerID) + ", " + rs.LayerName(layerID))
""" if not wasVisible:
    print(layerID) """

Once you pick the child you want to copy, it recursively grabs parents until it has them all. Then it will isolate them, allowing you to pick and choose how much of the history tree to copy.

v8 SVG: SelAllParentsCopyHistory
v7 PNG: SelAllParentsCopyHistory

import rhinoscriptsyntax as rs
import scriptcontext as sc
import eLib as e

def SelAllParents():
    objs = rs.GetObjects("Select objects to find all their parents", preselect=True)
    if objs: 
        count = len(objs)
        lastcount = 0
        while lastcount < count:
            lastcount = count
            rs.Command("_SelParents", False)
            count = len(rs.SelectedObjects(False, False))
        count = len(rs.SelectedObjects(False, False))
        if 0 < count:
            rs.Command("Isolate", True)

if (__name__ == "__main__"):

Sometimes I want to copy/paste history to the same or a different layer. This gives you the choice. The easiest way I could think of is to change the current layer name before it copies, change it back, then paste. If you have child layers involved, change only the parent layer first. You will get two layers with an identical sub-tree structure.

I put this on the right-click for the button (that’s what the plus sign is for).

import rhinoscriptsyntax as rs
import scriptcontext as sc
import eLib as e

def renameLayers():
    layersToRename = []
    prompt = "Should pasted objects be on a different layer?"
    while True:
        answer = rs.GetString(prompt, "No", ["Yes"])
        if answer == "Yes":
            layerID = rs.GetLayer("Select layer to rename:")
            if not layerID:
            oldName = rs.LayerName(layerID)
            newName = rs.GetString("New Layer name for pasted objects", e.removeLayerParents(oldName) + "-Copy")
            newName = str.replace(oldName, e.removeLayerParents(oldName), newName)
            if not newName:
            layersToRename.append(e.LayerName(layerID, oldName, newName))
            prompt = "Would you like to change any other layers?"
    return layersToRename

def CopyWithHistory():

    objs = rs.GetObjects("Select objects to copy with history", preselect=True)
    if objs: 
        layersToRename =  renameLayers()

        if 0 < len(layersToRename):
            for layer in layersToRename:
               e.renameLayer(layer.oldName, layer.newName)

        rs.Command("_CopyToClipboard", False)
        if 0 < len(layersToRename):
            for layer in layersToRename:
               e.renameLayer(layer.newName, layer.oldName)
        rs.Command("_Paste", False)
        pastedCount = len(rs.SelectedObjects())
        rs.Command("_SelObjectsWithHistory", False)
        parentCount = len(rs.SelectedObjects())
        strResponse = "Copied {0:d} with history. {1:d} root parents without history are selected and can be moved to a new location.".format(pastedCount, parentCount)

if (__name__ == "__main__"):

This reverts the selectability of everything to before you ran

I put this on the right-click for the button (that’s what the red arrow is for).

import rhinoscriptsyntax as rs
import scriptcontext as sc
import eLib as e

def UndoAllSelectable():

    if sc.sticky.has_key(e.layerStatesKey):
        layerStates = sc.sticky[e.layerStatesKey]
        if isinstance(layerStates, list):
            for layer in layerStates:
                if isinstance(layer, e.LayerState) and rs.IsLayer(layer.layerId):
                    rs.LayerVisible(layer.layerId, layer.visible)
                    rs.LayerLocked(layer.layerId, layer.locked)
        del sc.sticky[e.layerStatesKey]

    if sc.sticky.has_key(e.hiddenObjsKey):
        hiddenObjs = sc.sticky[e.hiddenObjsKey]
        if isinstance(hiddenObjs, list):
            print("Hid " + str(len(hiddenObjs)) + " objects.")
        del sc.sticky[e.hiddenObjsKey]

    if sc.sticky.has_key(e.lockedObjsKey):
        lockedObjs = sc.sticky[e.lockedObjsKey]
        if isinstance(lockedObjs, list):
            print("Locked " + str(len(lockedObjs)) + " objects.")

        del sc.sticky[e.lockedObjsKey]

if (__name__ == "__main__"):