C4D Python Fun: Copying Things

Copy this into those Ever had to sit around copying similar things into a lot of nulls? It's boring! I made this script for Cinema 4D to help out on a project where Mograph was almost (but not quite) what I needed, hence all the copying. I've polished it up so it's a bit friendlier, and I've posted it here as a freebie.

To install the script:
1 Download it here
2 Unzip it into your scripts folder (eg "C:\Program Files\MAXON\CINEMA 4D R13\library\scripts")

To use the script:
1 Select everything you want to copy your object into.
2 Control-click to add the object you want to make copies of to your selection (make sure to select this one last).
3 Run the script (click 'Script' > 'User Scripts' > 'copy_this_into_those').
4 Select your options from the dialogue, and click ok - the script will copy your last selection into all your other selected objects.

Keep reading if you'd like to go through the script's inner workings. This isn't so much a tutorial as it is a detailed explanation of the code, I hope you're undaunted.



I've heavily commented the code, so if you're familiar with Python in C4D you can probably just read through the script without this guide. Here goes!

The main bit
I'm going to start from the last half of the script - this is the delicious meaty bit where all the hard work is done. After that I'll cover the GUI, which appears first in the script file.

The first interesting thing we do is on line 78, where we grab all the selected objects using the function GetActiveObjects. This returns a list that we'll iterate over later, and by passing 'c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER' we can ensure that objects in this list appear in the order they were selected by the user - otherwise the list will be in the order those objects appear in the Object Manager.
75#This is where the action happens
76 def main():
77     #Get the selected objects, in the order in which they were selected
78     selection = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
Using the builtin Python function 'len' we can find out the length of the selection list - we need at least two objects, one to copy from, plus one or more to copy to, so if there's less than two we use 'print' to send a message to the console and then quit the script by calling 'return'.
80    #If there are less than 2 objects, quit with a useful message
81     if len(selection) < 2:
82         print "Please select at least 2 objects"
83         return
Here we open the options dialogue window. I'll cover the GUI bits properly later, but for now we just need to know that the script will pause after opening the dialogue on line 87 until the user has clicked either 'OK' or 'Cancel' in the window we open (because we're requesting a 'modal' dialogue by using the option 'c4d.DLG_TYPE_MODAL', which just means that everything else stops until the user finishes with the window). If the user clicks 'Cancel', we quit (again using 'return') before changing anything.
85    #Open the options dialogue to let users choose their options
86     optionsdialog = OptionsDialog()
87     optionsdialog.Open(c4d.DLG_TYPE_MODAL, defaultw=300, defaulth=50)
88     #Quit if the user has clicked cancel
89     if not optionsdialog.ok:
90         return
Now we get the object that the user selected last, that's the one we're going to copy later. I'm using 'len' again, 'len(selection)-1' is the index of the last object in the selection list. If you want to use the first object the user selected instead you could replace line 93 with 'objectoriginal = selection[0]'.
92    #The object that was selected last is the original we want to copy
93     objectoriginal = selection[len(selection)-1]
After we have the object we want to copy, we also get it's global matrix using 'GetMg()' - this is the position, scale and rotation coordinates of the original object in world space, regardless of the coordinates of any of its parents.
94    #Also make a copy of the original's global matrix,
95     #in case the user has asked to preserve that in the copies
96     originalmatrix = objectoriginal.GetMg()
It's a good idea to add undo information to your scripts, especially if other people are using them, and especially if you're deleting things! Cinema 4D doesn't add this automatically, so if you don't do it and the user changes their mind after using your script, they may get weird results when they try to undo. The first thing you need to do to tell Cinema 4D how to undo your script is call 'doc.StartUndo()'
98    #Add undo information between 'doc.StartUndo()' and 'doc.EndUndo()'
99     #so C4D knows how to reverse what we've done
100     doc.StartUndo()
The for loop from line 103 to 119 repeats for every number from 0 up to (but not including) 'len(selection)-1' - the same bit of code we used before to find the last item in the selection list. If you changed line 93 above to copy the first selected object instead of the last, you would have to change line 103 to 'for i in xrange(1, len(selection)):'
101    #Iterate over all the other objects in the selection (other than the last one),
102     #putting a new copy of the original into each one
103     for i in xrange(len(selection)-1):
If the user selects the 'delete existing children' option, we delete any children of the objects we're going to copy into before we do the copying. The 'GetChildren' on line 106 gives us all the children of the current selection.
104        #Delete existing children of target objects, if the user has selected that option       
105         if(optionsdialog.option_deleteold):
106             children = selection[i].GetChildren()
107             for child in children:
This is our first undo instruction - 'AddUndo' tells Cinema 4D the type of action we're performing ('c4d.UNDOTYPE_DELETE' tells C4D we're deleting something), and the object we're performing it on. After that, we can delete the object with 'Remove()'.
108                #We're about to delete an object, this 'AddUndo' allows C4D to reverse that
109                 doc.AddUndo(c4d.UNDOTYPE_DELETE, child)
110                 child.Remove()
This is the bit where we actually copy stuff! 'objectoriginal.GetClone()' gives us a new object based on the original we saved earlier, which we then put into place in the hierarchy using 'InsertUnder' (this makes our clone a child of whatever object we pass as a parameter to the InsertUnder function).
111        #Get a copy of the object, and put it under the current object
112         objectcopy = objectoriginal.GetClone()
113         objectcopy.InsertUnder(selection[i])
Our second undo instruction tells C4D that we've added a new object ('c4d.UNDOTYPE_NEW'). Notice that the new object instruction comes after the object is added, unlike the delete instruction which came before the object was deleted.
114        #This 'AddUndo' allows C4D to reverse the new object creation we just did
115         doc.AddUndo(c4d.UNDOTYPE_NEW, objectcopy)
If the user has asked to preserve the global coordinates of the original object, we set it's global matrix to the original matrix we saved earlier on line 119. Note that if any of the position, rotation or scale tracks have keyframes, they'll snap back to local coordinates despite this code.
116        #Reset the copies' position, scale and rotation coordinates to the same
117         #global coordinates as the original, if the user has selected that option
118         if(optionsdialog.option_global):
119             objectcopy.SetMg(originalmatrix)
We've finished the loop, and now we call 'doc.EndUndo()' to let Cinema 4D know we're done changing things and it can save the undo information - everything between 'StartUndo' and 'EndUndo' can be undone with a single undo operation (so if we've made a hundred copies, the user doesn't have to press ctrl-z a hundred times to get rid of them all).
120    doc.EndUndo()
Calling 'c4d.EventAdd()' forces C4D to update, so we can see our changes in the manager. Finally we print a line to the console to let the user know how many copies were made.
121    #This updates C4D so we can see what we've done straight away
122     c4d.EventAdd()
123     #Print a handy message about how many copies were made
124     print str(len(selection)-1) + " copies"
This is the last thing that appears in the script, but it's actually where it all begins - this bit of code calls the 'main()' function I've just been describing.
126#This is where the script starts
127 if __name__=='__main__':
128     main()

The GUI
Wowsers, if you've got any energy left you can read through this bit about the GUI. For simple scripts you can often set your own default settings in code, but sometimes you might want to be able to choose some settings each time the script is run. Instead of changing the code each time, you could add a GUI to ask for settings, like we're doing here. Ok, we're going back to the top of the script for the GUI explanation!

Any GUI widget (buttons, textboxes, checkboxes, dropdown menus etc) that you add in C4D need to have a unique ID number to access them. They're declared globally (outside any functions or classes) so that you can access them from any part of your script.
33#Unique id numbers for each of the GUI elements
34 TEXT_INFO = 1000
35 GROUP_OPTIONS = 10000
36 OPTION_GLOBAL = 10001
37 OPTION_DELETEOLD = 10002
38 GROUP_BUTTONS = 20000
39 BUTTON_OK = 20001
40 BUTTON_CANCEL = 20002
41 #This class defines the dialogue that pops up to request user options
42 class OptionsDialog(gui.GeDialog):
43     #Add all the items we want to show in the dialogue box
This next function (CreateLayout) is where all the widgets are added to your dialogue box. I've started by adding a left-aligned static text box (self.AddStaticText - the alignment is set in the second parameter, c4d.BFH_LEFT).
44    def CreateLayout(self):
45         #A text message to remind the user what this script does
46         self.AddStaticText(TEXT_INFO, c4d.BFH_LEFT, name="Copy this into those - copies your last selected object into all the other selected objects")
You can get more control over the layout using groups - here I've added a left aligned group for the checkboxes with 1 column and 2 rows (lines 49 to 52), followed by a centre aligned group (c4d.BFH_CENTER) for the 'OK' and 'Cancel' buttons with 2 columns and 1 row (lines 54 to 57). The function returns 'True' to let C4D know everything went ok.
47        #Options - checkboxes to select the script options
48         #Groups allow us to lay out widgets in rows and columns, select their alignment, etc
49         self.GroupBegin(GROUP_OPTIONS, c4d.BFH_SCALE|c4d.BFH_LEFT, 1, 2)     
50         self.AddCheckbox(OPTION_GLOBAL, c4d.BFH_LEFT, 0,0, name="Preserve global coordinates (won't work for keyframed coordinates)")
51         self.AddCheckbox(OPTION_DELETEOLD, c4d.BFH_LEFT, 0,0, name="Delete existing children of target objects")
52         self.GroupEnd()
53         #Buttons - an OK and a CANCEL button
54         self.GroupBegin(GROUP_OPTIONS, c4d.BFH_CENTER, 2, 1)
55         self.AddButton(BUTTON_OK, c4d.BFH_SCALE, name="OK")
56         self.AddButton(BUTTON_CANCEL, c4d.BFH_SCALE, name="Cancel")
57         self.GroupEnd()
58         return True
Next up is the 'Command' function. If the user clicks a widget, this function gets called with the id of the widget they clicked - we saved all those unique ID numbers into variables earlier, so now we can tell which widget we're reacting to.
60    #This is where we react to user input (eg button clicks)
61     def Command(self, id, msg):
62         if id==BUTTON_CANCEL:
63             #The user has clicked the 'Cancel' button
64             self.ok = False
65             self.Close()
66         elif id==BUTTON_OK:
67             #The user has clicked the 'OK' button
68             self.ok = True
69             #Save the checkbox values so that the rest of the script can access them
70             self.option_global = self.GetBool(OPTION_GLOBAL)
71             self.option_deleteold = self.GetBool(OPTION_DELETEOLD)
72             self.Close()
73         return True
If the user clicks the 'Cancel' button, we set self.ok to False (self.ok is a variable I've made up - we can access it after the dialogue closes to check if the user has pressed 'OK' or 'Cancel'). If the user clicks 'OK' self.ok is set to True, and additionally we grab the checkbox values with 'GetBool' and put them in our own variables we can get to from the 'main()' function when we're checking what options the user has selected.

That's it! I know that was pretty epic, I hope you're still awake and that this comes in handy sometime. Check out the links on the resources page for more Python tutorials from other people.
Posted February 10, 2013