Python Script Advice

I’m new to Python scripting and managed to get together a few scripts that work, but I’m wondering if there might be a better way to implement them or if this is the way it should be done. If there is a better way to do things, then learning about it now would be better than writing tons of scripts in ways that could be better, so now is the time to learn.

This script is meant to export a selection to an Iges file. What I am selecting as input to this are some blocks as well as some things not in a block. The blocks are nested and there are some text objects inside one of the nested blocks that I need to explode before exporting, otherwise It doesn’t get exported because the Iges file format has no way to store text.

What I have been doing by hand is exploding 3 times, and that gets the text exploded, then exporting my file, then undoing 4 times… once to undo the export, which really does nothing, because the file has been created, and 3 more times to undo the 3 explodes.

So, I quickly found out a few things…

  1. Undo from inside a python script is either completely impossible, or very difficult.
  2. Undo after running a script undoes everything that happened in the script.
  3. Running anything after a script with a button doesn’t work

so my initial thought was that it would be GREAT to just undo the entire script… then I can do whatever I want, not bother to keep track of anything or make it complicated, get my file exported, then just do one undo and everything gets undone.
I do something similar in rhino all the time, I do a BlockEdit, then do whatever I want to the contents of the block to extract various geometry, and completely destroy everything in there to get what I want, copy what I want to the clipboard, then hit the X on the BlockEdit, and poof, nothing I did in there mattered at all… then Paste.
So since undoing an Export really does nothing, because the file is saved already, I can just undo my entire script, and everything is great. So I tested this out and it does work… I run the script, then run undo and it’s fine… but I want the undo to happen along with the script so I don’t have to remember to do it.

so I tried assigning it to a button like this:

! _-RunPythonScript (
import System
import Rhino
import rhinoscriptsyntax as rs

def Export_Drill():
    #Restore Layer state
    plugin = rs.GetPlugInObject("Rhino Bonus Tools")
    if plugin is not None:
        plugin.RestoreLayerState("Wall Notch")
    script = "_-SelLayer \"{0}\" _Enter".format("Drill Alignment")
    Rhino.RhinoApp.RunScript(script, True)
    script = "_-Explode"
    Rhino.RhinoApp.RunScript(script, True)
    Rhino.RhinoApp.RunScript(script, True)
    Rhino.RhinoApp.RunScript(script, True)
    folder = r'M:\CNC_Data\Drill\2x4'
    name = rs.DocumentName()
    extension = ".igs"
    path = System.IO.Path.Combine(folder, name)
    path = System.IO.Path.ChangeExtension(path, extension)
    print path
    script = "_-Export _Pause \"{0}\" _Enter".format(path)
    Rhino.RhinoApp.RunScript(script, True)

Export_Drill()
)
Undo

But the Undo at the end never runs… in fact nothing that I put under the script ever runs. Is there a way around this?

So, then it was suggested that I make a copy then do everything to the copy and delete the copy when I’m done, and while that is less than ideal, it does work:

import System
import Rhino
import rhinoscriptsyntax as rs

def Export_Cut():
    script = "_-CopyToClipboard"
    Rhino.RhinoApp.RunScript(script, True)
    script = "_-SelNone"
    Rhino.RhinoApp.RunScript(script, True)
    script = "_-Paste"
    Rhino.RhinoApp.RunScript(script, True)
    script = "_-Explode"
    Rhino.RhinoApp.RunScript(script, True)
    Rhino.RhinoApp.RunScript(script, True)
    Rhino.RhinoApp.RunScript(script, True)
    folder = r'M:\CNC_Data\Cut\2x4'
    name = rs.DocumentName()
    extension = ".igs"
    path = System.IO.Path.Combine(folder, name)
    path = System.IO.Path.ChangeExtension(path, extension)
    print path
    script = "_-Export _Pause \"{0}\" _Enter".format(path)
    Rhino.RhinoApp.RunScript(script, True)
    script = "_-Delete"
    Rhino.RhinoApp.RunScript(script, True)
Export_Cut()

The reason I say it is less than ideal is that after I do the paste, I have now 2 copies of everything, and if I try to do something like select by layer, I will get both copies, and if I’m not really careful with what is selected when I do the delete, I might delete some of the original geometry.

So then I thought instead of doing it that way, maybe I could have it create a block around the copy, then do a BlockEdit to do whatever to the copy, do my export, then close the BlockEdit and delete the block… this way none of my original geometry is a risk and any selections I do will only get the copy inside the block. But I tried to close the BlockEdit with a script before (not python, just commands in a button) and couldn’t figure out how to do that. I can issue the BlockEdit command… but there is no command I can give to exit BlockEdit.

So it’s starting to get complicated, the simplest solution is to just undo the entire script, but how can I accomplish that automatically?

I’m also wondering if there is a better way do make this script at all. I am basically just passing along commands to Rhino exactly the same as it would be to just stack them up in a button, except for where I am getting the document name and creating a destination for the export.

I’ve been searching though forum posts and it seems like there is some way to create geometry and work on things without commit it to the Rhino drawing, but I’m not understanding how that all works, or how I might use that to make a temporary copy that would evaporate when I end the script.

Any advice is very much appreciated.

If the processing is necessarily destructive, instead of Undo, I recommend copying the .3dm file, doing everything on the copy and deleting that copy afterwards.

I’ve not tested it, but I think an undo command in a script would only undo the previous sub-command in the same script. In coding and even in scripting it’s more common to simply not do the thing in the first place that you want to undo (if the point isn’t to use side effects that the undo doesn’t clear up, but doing so strikes me as designing in an external dependency on a glitch or bug that could change in future). An undo in Rhino treats all the commands in a script as one compound command to undo. Otherwise there’s a minor logical paradox to resolve to decide what should happen when an undo undoes itself.

That would be so time consuming I wouldn’t need the script at all. unless I could somehow make the script make the copy of the .3dm file and then delete the copy… and then put me back on the original so I can export the next part… and even if I could get this all to work, it would still not help, because my .3dm file takes 4 minutes to load… it’s very complex

Yes I realized this… and that is exactly what I want to do define a button with:

! _-RunPythonScript (
#some script
)
_-Undo

It will accomplish what I want because the export cannot be undone… the file exported will still be there.

If I run the script and do the undo myself, then it’s great… I just want the undo to happen when I push the button, but any commands I put after the script never execute. If I can automate the _-Undo after the script runs, then it is very fast and perfectly safe… but if the undo is not automated, then I can make a mistake and get distracted and miss one undo… it would be easy to mess up when exporting literally hundreds of parts.

As far as I know, exporting to standard formats is not available in RhinoCommon, so we cannot extract the geometry and work on that … it would be much more clear and simple … anyway. :wink:

What I can think of is …
Using rs.IsBlock with rs.ExplodeBlockInstance to explode blocks …
and rs.IsText with rs.ExplodeText to explode text objects.
Then export just like you are doing it now.
(All the above things inside the script)

And then trying to Undo the whole script adding an _Undo command aflter the _RunPythonScript command in the button.

… But I haven’t tried that.
Not sure whether it works or not … sorry. :blush:

HTH

As far as I know there is no such thing as exporting objects from RhinoCommon directly to IGES, so I think the best option you have is to

  • Iterate over the Object Table, and extract all geometry you need into a new array
  • make sure nothing is selected
  • write the array as new objects to the RhinoDoc (making them selected)
  • export the selection
  • delete the selection

But you will need to dive into working with RhinoCommon quite a bit.

Here is an example (not dealing with text, I will leave that to you)

import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

def extract(obj):
    objs = obj.GetSubObjects()
    for o in objs:
        if type (o) == Rhino.DocObjects.InstanceObject:
            o = extract(o)
        else:
            array_objs.append(o)

objs = sc.doc.Objects
array_objs = []
for obj in objs:
    if type(obj) == Rhino.DocObjects.InstanceObject:
        extract(obj)
    else:
        array_objs.append(obj)
rs.EnableRedraw(False)        
rs.UnselectAllObjects()
for obj in array_objs:
    rs.SelectObject(sc.doc.Objects.Add(obj.Geometry))
rs.Command("-_Export d:\\test.igs Enter")
rs.Command("_Delete")
rs.EnableRedraw(True)

I think - mind you, I haven’t tested - that if you make an alias for the script, you can then use the alias in a macro with other Rhino commands. Someone correct me if I’m wrong on this…

unless I could somehow make the script make the copy of the .3dm file

import shutil
shutil.copy2(source, dest)

and then delete the copy

import os
os.remove(dest)

Thank you very much. I will try to get this working. I think I should be able to even have something like this make the selections for me.

unfortunately, that doesn’t work… it works exactly the same as putting the script in the button, but in testing it, I discovered that commands put after the script do run… they just run WAY too early, they do not wait for the script to end… and they do not run before the script either, they run at the same time as the script and in fact it’s possible to completely screw up the script with any commands that are put after the script. In my opinion this is a bug, the script should hold up further commands in the button so that any commands following the script cannot corrupt the script… and of course if it acted this way then my Undo would work the way I want it to :slight_smile: I tried putting a pause and a multipause and they have no effect, everything after them just keeps on running.

@James_Parrott - Thank you for the info. How would I open the copy?

I’m not sure exactly, but you just script the commands into Rhino like any other. It’s probably just _File _Open or something.

Just case someone ends up here from doing a search I thought I would link what I figured out about why I can’t undo my script with the button:

Pretty much using Rhino.RhinoApp.RunScript() in a python script causes everything in the alias, or button or whatever to run immediately… so any undo would run way before the script finsihed… any command at all after the script in a button will run immediately when the first Rhino.RhinoApp.RunScript() is used and I can’t find a way to force a wait for the script to end… it’s some probably something very fundamental about the way Rhino.RhinoApp.RunScript() works.

So it’s completely impossible for me to use Export and then do an undo… unless I do the undo myself… but I might get distracted and forget… so the only real option is to with one method or another make a copy, work on the copy, then delete it.

Did you try rs.Command instead of RhinoApp.Runscript?

No, I didn’t know where was such a thing… but I just tried it now and it has the same results.

Is there an easy way to search through this list?

Often I have an idea what the name of a function should be, but I don’t know what category it’s in.

Can someone please explain how I can get into debug mode?
I click Start Debugging and I still see this:

image

Step Into, Step Over, ect are always greyed out

Also when I Click Start Debugging, my command history shows:
Command: -_EditPythonScript

( Debugging=On ): Debugging=Off
( Debugging=Off ): (
C:\Users\james\AppData\Roaming\McNeel\Rhinoceros\7.0\scripts\Export Wall Notch to Drill Folder.py

like it’s turning on debugging then turning it off immediately.

I would really like to single step through my script to follow the logic of it.

You need to add a red marker where you want to stop the script, click in the margin to add one:
image

Thank you, I didn’t expect a breakpoint would be required, my other compilers let me step into from the beginning, but I could just put a breakpoint on the first line and do the same thing.

I’m making progress on my script. I appreciate the help here. I’ve come up with a script that makes the selections for me, does the export, makes another set of selections and does a second export and it works pretty well, but there are some issues. Mainly that I can’t figure out a good way to clean up after this… so to bypass the problem, I’m just saving the file first, and reverting back to the saved copy after… kind of a long very slow Undo… but at least I can hit one button, then task switch to something else and when I come back both exports are done… and my file is even in the exact same state it was in before I executed the scripts, which is also nice… in fact its nicer than running the script then undo, because Undo after running a script that changes the layer state does not put the layer state back the way they were, so while all the geometry is all reset, it’s not exactly undoing the entire script.

The only issue is how slow it is to do the save and revert, so I would still really like to execute more instructions after the script, then I could just do Undo, and change layer states back to a default state.

Here is my new python script:

New Export Python Script - Click to open
import System
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

def SelObjsLayerAndSubs(layer):
    rs.ObjectsByLayer(layer, True)
    subLayers = rs.LayerChildren(layer)
    if(subLayers):
        #layer has one or more sublayers
        for sLayer in subLayers:
            #recurse
            SelObjsLayerAndSubs(sLayer)

def ExportProcedure(ExportType):
    #Restore Layer state
    plugin = rs.GetPlugInObject("Rhino Bonus Tools")
    if plugin is not None:
        if ExportType == "Drill":
            plugin.RestoreLayerState("Wall Notch")
            rs.ObjectsByLayer("Drill Alignment", True)        
        if ExportType == "Cut":
            plugin.RestoreLayerState("Compound Miter")
            SelObjsLayerAndSubs("Containment")
    rs.Command("_CopyToClipboard")
    rs.UnselectAllObjects()
    rs.Command("_Paste")
    rs.Command("_Explode")
    rs.Command("_Explode")
    rs.Command("_Explode")
    if ExportType == "Drill":
        folder = r'M:\CNC_Data\Struts\Drill\2x4\Iges Files'
    else:
        folder = r'M:\CNC_Data\Struts\Cut\2x4\Iges Files'
    name = rs.DocumentName()
    name = name.replace('Strut 2x4 ', '')
    extension = ".igs"
    path = System.IO.Path.Combine(folder, name)
    path = System.IO.Path.ChangeExtension(path, extension)
    if PrintExportDetails:
        print path
    rs.Command("_-Export _Pause \"{0}\" _Enter".format(path))
    rs.Command("_Delete")

def SelectZone(Zone):
    objs = sc.doc.Objects
    array_objs = []
    for obj in objs:
        if rs.IsBlockInstance(obj):
            array_objs.append(obj)
            if PrintSelectDetails:
                print(obj)
                print(rs.BlockInstanceName(obj))
                print(rs.BlockInstanceInsertPoint(obj))
                print("rs.BlockInstanceInsertPoint(obj) = ",rs.BlockInstanceInsertPoint(obj))
                print("(-3.4,0,0) = ",Rhino.Geometry.Point3d(-3.4,0,0))
            if rs.BlockInstanceInsertPoint(obj) == Rhino.Geometry.Point3d(-3.4,0,0):
                if PrintSelectDetails:
                    print ("Left Zone")
                if Zone == "Left":
                   rs.SelectObject(obj)
            else:
                if rs.BlockInstanceInsertPoint(obj) == Rhino.Geometry.Point3d(0,0,0):
                    if PrintSelectDetails:
                        print ("Right Zone")
                    if Zone == "Right":
                        rs.SelectObject(obj)
                else:
                    if PrintSelectDetails:
                        print ("Other Block Ignored")
def ExportEverything():
    rs.Command("_Save")
    rs.UnselectAllObjects()
    SelectZone("Left")
    ExportProcedure("Drill")
    rs.UnselectAllObjects()
    SelectZone("Left")
    SelectZone("Right")
    ExportProcedure("Cut")
    rs.UnselectAllObjects()
    rs.Command("_-Revert")

PrintSelectDetails = False
PrintExportDetails = True
ExportEverything();

I would like to make a script that cleans up after itself, so I can avoid the revert, because that takes quite a while… but at least it’s safe.
an undo after the script does clean everthing up nicely, but I can’t automate that… maybe the developers at Rhino can figure out how to delay commands after the scrip in the button even if something like rs.Command() is used… but even if that could happen, it would be a solution for the future.

What I want to export is complicated, that is why I am writing a script to automate the process.
I have not found a good way to clean up after this… here’s the issue: what I need to export are blocks that contain nested blocks, and those blocks all have a lot of objects on different layers, AND objects not in a block also on different layers.
I don’t want to export everything on all the layers, because my Iges files are tool complicated…I just want to export what is actually needed by the CAM program for each process. each process requires different layers, so I have defined Layer States for each process. I need multiple layers and I need to retain the layer information in the Iges file.
When I select my main blocks, make a copy, explode them, when I change layer states, It will deselect things that are on layers that get turned off, and it is at this point that I have cluttered up my file with extra objects that I can’t even see, because they are on hidden layers, and it’s impossible to re-select just the copies to delete them, they are combined with objects that were never in a block to begin with. Even If I could somehow keep them selected, I wouldn’t want to because I DON’T want them exported and export exports the selected items, which is what I am wanting to do.

So in this situation, making a copy and then deleting the copy is not the same as being able to do an Undo because as soon as I hide the items I don’t want to export, I can’t distinguish them from any other objects anymore.

I think maybe there is way I can use @Gijs method of making an array to keep track all the copied objects so only the copies get deleted, but that would be more advanced than I am capable of at the moment, and it would not be as elegant or as easy to implement simply doing an undo after the script runs. One issue I have with that is everything ends up on the current layer… I’m not sure how to retain the layers or how to get exactly what I want in the array. I’m sure there is a way, but it’s something I’ll have to work on.

For now Saving right before the export and Reverting at the end is the safest way, but it is very slow, so it would be great if I had a effective way to clean up after all this exploding and layer changing.

I have a new idea… a way to achieve this and clean up.

After I have everything selected, I could move the selections to a specific location where there isn’t anything, then do my explodes, move the remaining selected items back to where they belong and export them, then delete them, then turn on all layers, select everything in the explode location that was on hidden layers and delete everything.

So, the question is there some way to select everything inside a specific region with a script? like mouse dragging a selection box around it all, but without using a mouse?
Or perhaps select everything where Z > 20" or something like that?

Hello,
I think you can use this method to get everything within a 3D cube ObjectsByRegion

That looks exactly like what I need. Thank you for the suggestion, I will try it.

Now I have an even newer idea! Use Lock to isolate the copy of everything, from the original.

    rs.Command("_CopyToClipboard")
    rs.Command("_-Layer _On * _Enter ")
    rs.Command("_SelAll")
    rs.Command("_Lock")
    rs.Command("_Paste")
...
   #Explode things, do whatever to get my export done#
...
    rs.Command("_-Layer _On * _Enter ")
    rs.Command("_SelAll")
    rs.Command("_Delete")
    rs.Command("_Unlock")

This works quite well and removes things from hidden layers.
@Gijs: Thank you for the lock idea from here:

Here's the script
import System
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

def SelObjsLayerAndSubs(layer):
    rs.ObjectsByLayer(layer, True)
    subLayers = rs.LayerChildren(layer)
    if(subLayers):
        #layer has one or more sublayers
        for sLayer in subLayers:
            #recurse
            SelObjsLayerAndSubs(sLayer)

def RestoreLayerState(LayerStateName):
    #Restore Layer state
    plugin = rs.GetPlugInObject("Rhino Bonus Tools")
    if plugin is not None:
        plugin.RestoreLayerState(LayerStateName)

def ExportProcedure(ExportType):
    if ExportType == "Drill":
        RestoreLayerState("Wall Notch")
        rs.ObjectsByLayer("Drill Alignment", True)        
    if ExportType == "Cut":
        RestoreLayerState("Compound Miter")
        SelObjsLayerAndSubs("Containment")
    rs.Command("_CopyToClipboard")
    rs.Command("_-Layer _On * _Enter ")
    rs.Command("_SelAll")
    rs.Command("_Lock")
    rs.Command("_Paste")
    rs.Command("_Explode")
    rs.Command("_Explode")
    rs.Command("_Explode")
    if ExportType == "Drill":
        rs.Command("_Move -3.4,0 3.5,0")
        rs.Command("_Rotate3D 0 0,100 90")
        folder = r'M:\CNC_Data\Struts\Drill\2x4\Iges Files'
    else:
        folder = r'M:\CNC_Data\Struts\Cut\2x4\Iges Files'
    name = rs.DocumentName()
    name = name.replace('Strut 2x4 ', '')
    name = name.replace('Strut 2x6 ', '')
    extension = ".igs"
    path = System.IO.Path.Combine(folder, name)
    path = System.IO.Path.ChangeExtension(path, extension)
    if PrintExportDetails:
        print path
    rs.Command("_-Export _Pause \"{0}\" _Enter".format(path))
    rs.Command("_-Layer _On * _Enter ")
    rs.Command("_SelAll")
    rs.Command("_Delete")
    rs.Command("_Unlock")

def SelectZone(Zone):
    objs = sc.doc.Objects
    array_objs = []
    for obj in objs:
        if rs.IsBlockInstance(obj):
            array_objs.append(obj)
            if PrintSelectDetails:
                print(obj)
                print(rs.BlockInstanceName(obj))
                print(rs.BlockInstanceInsertPoint(obj))
                print("rs.BlockInstanceInsertPoint(obj) = ",rs.BlockInstanceInsertPoint(obj))
                print("(-3.4,0,0) = ",Rhino.Geometry.Point3d(-3.4,0,0))
            if rs.BlockInstanceInsertPoint(obj) == Rhino.Geometry.Point3d(-3.4,0,0):
                if PrintSelectDetails:
                    print ("Left Zone")
                if Zone == "Left":
                   rs.SelectObject(obj)
            else:
                if rs.BlockInstanceInsertPoint(obj) == Rhino.Geometry.Point3d(0,0,0):
                    if PrintSelectDetails:
                        print ("Right Zone")
                    if Zone == "Right":
                        rs.SelectObject(obj)
                else:
                    if PrintSelectDetails:
                        print ("Other Block Ignored")
def ExportEverything():
    #rs.Command("_Save")
    rs.UnselectAllObjects()
    SelectZone("Left")
    ExportProcedure("Drill")
    rs.UnselectAllObjects()
    SelectZone("Left")
    SelectZone("Right")
    ExportProcedure("Cut")
    rs.UnselectAllObjects()
    RestoreLayerState("Compound Miter")
    #rs.Command("_-Revert")

PrintSelectDetails = False
PrintExportDetails = True
ExportEverything();