GetObject selection behavior

I finally decided to try some Rhino (6/mac) scripting, with a simple tool to export paths as gcode. It mostly works well, but I have had a lot of difficulty understanding how to get the selection behavior I want.

It would be great if anyone could suggest a resource that explains this topic in general. The RhinoCommon documentation lists a ton of methods and properties related to selection, but it is not at all clear how they interact, and I managed to crash Rhino quite a lot by trying to experiment.

My specific, current problem is that I need the user to pick objects one by one in a specific order, and I want to select the objects as they are picked – partly to provide feedback, and partly because it shouldn’t be possible to pick the same object twice. The OneByOnePostSelect property gives me part of this behavior, but the objects are not visibly selected when I pick them, and if I call rhinoscriptsyntax.SelectObject() on the picked ObjectRef, nothing happens.

I’ve attached my script below – hopefully it is understandable, but it will probably be obvious to Rhino experts that I haven’t used these APIs (or python) before!

import rhinoscriptsyntax as rs
import Rhino

gcodeFile = None
feedrate = 1500.0
spindle = 10000.0
retractHeight = 6.0
tool = 0
jobOrigin = 0
firstPath = True
gcodeState = dict(
    x = 0,
    y = 0,
    z = 0,
    f = 0
    )



# prompt for an output location and setup origin, and then repeatedly
# prompt for toolpaths and machine settings to write to the file in order
def event_loop():
    global gcodeFile
    global feedrate
    global spindle
    global retractHeight
    global tool
    global jobOrigin
    global firstPath
    filename = rs.SaveFileName(
        title = "Export G Code",
        filter = "G Code File (*.nc)|*.nc|All files (*.*)|*.*||",
        filename = "Untitled",
        extension = "nc"
        )
    if not filename: return
    gcodeFile = open(filename, "w")
    jobOrigin = rs.GetPoint("Select toolpath origin")
    prompt = Rhino.Input.Custom.GetObject()
    feedrateOption = Rhino.Input.Custom.OptionDouble(feedrate, 1.0, 5000)
    prompt.AddOptionDouble("Feed", feedrateOption, "New feed rate (mm/min)")
    spindleOption = Rhino.Input.Custom.OptionDouble(spindle, 0, 10000.0)
    prompt.AddOptionDouble("Speed", spindleOption, "New spindle speed (rpm)")
    retractOption = Rhino.Input.Custom.OptionDouble(retractHeight, 0, 100.0)
    prompt.AddOptionDouble("RetractHeight", retractOption, "Safe retract height (mm)")
    toolOption = Rhino.Input.Custom.OptionInteger(tool, 0, 1000)
    prompt.AddOptionInteger("Tool", toolOption, "Tool number")
    prompt.AcceptNothing(True)
    prompt.EnablePreSelect(False, True)
    prompt.OneByOnePostSelect = True
    prompt.SetCommandPrompt("Select first toolpath curve")
    prompt.GeometryFilter = Rhino.DocObjects.ObjectType.Curve
    while True:
        rc = prompt.Get()
        if prompt.CommandResult() != Rhino.Commands.Result.Success:
            return prompt.CommandResult()
        if rc == Rhino.Input.GetResult.Object:
            curveRef = prompt.Object(0)
            if not curveRef: return Rhino.Commands.Result.Failure
            append_toolpath(curveRef)
            if firstPath:
                firstPath = False
                prompt.SetCommandPrompt("Select next toolpath curve")
            continue
        elif rc == Rhino.Input.GetResult.Option:
            opt = prompt.OptionIndex()
            if feedrateOption.CurrentValue != feedrate:
                feedrate = feedrateOption.CurrentValue
            if spindleOption.CurrentValue != spindle:
                spindle = spindleOption.CurrentValue
                if not firstPath: gcodeFile.write("M3 S{}\n".format(gcs(spindle)))
            if retractHeight != retractOption.CurrentValue:
                retractHeight = retractOption.CurrentValue
            if tool != toolOption.CurrentValue:
                tool = toolOption.CurrentValue
                gcodeFile.write("M6 T{:d}\n".format(tool))
            continue
        elif rc == Rhino.Input.GetResult.Nothing:
            close_file()
            print ("Exported GCode to "+filename)
            break
        elif rc == Rhino.Input.GetResult.Cancel:
            #TODO use a temp file so it's actually possible to cancel export...
            close_file()
            print ("GCode Export cancelled")
        break

    return Rhino.Commands.Result.Success




#format numbers for CNC without wasting bytes
def gcs(num): return "{:.99g}".format(round(num,3))

#convert Rhino units to mm
def rtomm(n): return n * rs.UnitScale(2)

#convert mm to Rhino units
def mmtor(n): return n / rs.UnitScale(2)




# write out G code for initial setup and tool positioning
# (once we know where the first toolpath begins)
def write_prefix(startX=0, startY=0):
    global gcodeState
    gcodeFile.write("\n".join((
        "G21 (distances in mm)",
        "G90 (absolute coords)",
        "G17 (use XY plane for arcs)",
        "G0 Z" + gcs(retractHeight) + " (rapid to safe height)",
        "G0 X" + gcs(startX) + " Y" + gcs(startY) + " (rapid to start XY)",
        "M3 S" + gcs(spindle) + " (run spindle clockwise)",
        "",
        )))
    gcodeState['x'] = startX
    gcodeState['y'] = startY
    gcodeState['z'] = retractHeight
    return



# write out G code for a toolpath, prepending commands to
# move the tool safely (you hope) from its previous position
def append_toolpath(curveRef):
    global gcodeState
    curve = curveRef.Curve()
    if rs.IsPolyline(curve):
        polyline = curve
    else:
        polyline = rs.ConvertCurveToPolyline(curve, 5.0, mmtor(0.01), False)
    points = rs.PolylineVertices(polyline)
    point = rs.PointSubtract(points[0], jobOrigin)
    if firstPath:
        write_prefix(point.X, point.Y)
    else:
        gcodeFile.write("G1 Z{}\nG0 X{} Y{}\n".format(
            gcs(retractHeight),
            gcs(point.X),
            gcs(point.Y)
            ))
    gcodeFile.write("\n(toolpath {}mm)\n".format(gcs(rs.CurveLength(curve))))
    for i in range(len(points)):
        point = rs.PointSubtract(points[i], jobOrigin)
        write_G1(point)
    if not rs.IsPolyline(curve): rs.DeleteObject(polyline)
    rs.SelectObject(curveRef)
    return


# write out a G1 command (single linear move) in a compact form,
# setting the X,Y,Z and F parameters only where they have changed
def write_G1(dest):
    epsilon = mmtor(0.001)
    if rs.Distance(dest, (gcodeState['x'],gcodeState['y'],gcodeState['z'])) < epsilon:
        return
    cmd = "G1 "
    sep = ""
    if abs(dest.X - gcodeState['x']) > epsilon:
        cmd += "X" + gcs(rtomm(dest.X))
        gcodeState['x'] = dest.X
        sep = " "
    if abs(dest.Y - gcodeState['y']) > epsilon:
        cmd += sep + "Y" + gcs(rtomm(dest.Y))
        gcodeState['y'] = dest.Y
        sep = " "
    if abs(dest.Z - gcodeState['z']) > epsilon:
        cmd += sep + "Z" + gcs(rtomm(dest.Z))
        gcodeState['z'] = dest.Z
        sep = " "
    if feedrate != gcodeState['f']:
        cmd += sep + "F" + gcs(feedrate)
        gcodeState['f'] = feedrate
    gcodeFile.write(cmd + "\n")
    return



# move tool to a safe height, write final commands and close file
def close_file():
    gcodeFile.write ("G1 Z" + gcs(retractHeight) + "\n\n")
    gcodeFile.write ("M5 (stop spindle)\nM30 (program end)\n")
    gcodeFile.close()
    return



if(__name__ == "__main__"):
    event_loop()

gcExport.py (6.2 KB)

Hi @bobtato, try if attached script is helpful: GetObject_OneByOnePostSelect.py (1.1 KB)

_
c.

2 Likes

Thanks @clement – using GetMultiple() instead of Get() was a helpful clue!

In your example, it gives the right behavior, but in my script it still didn’t work, because I want to process each object as it is selected. However, starting from there, I think I figured out how the process works (in case anyone else finds the documentation unhelpful)…

  • GetMultiple(min,max) lets the user select objects indefinitely, and returns only if:

    1. the user presses enter
    2. the maximum number of objects is selected (if max!=0); or
    3. the user sets an option, presses ctrl-Z, cancels the command etc.
  • It compares min and max to the total number of selected objects, including those that were already selected when it was invoked; the parameters don’t directly relate to how many objects are selected in this invocation of the method.

  • If the user presses enter, and the number of selected objects is less than min, then GetMultiple() returns Rhino.Input.GetResult.Nothing, even if objects were added to the selection.

  • Conversely, if the user presses enter, and the total number of selected objects is not less than min, then it returns Rhino.Input.GetResult.Object even if nothing new was selected.

  • The effect of OneByOnePostSelect, as far as I can tell, is just that it disables window and crossing selection.

So, because I want the method to accept exactly one new object and then return, in each iteration of my event loop I set the values of both min and max to L+1, where L is the current number of objects. This means that it returns Object when a single object was clicked, and Nothing if the user presses enter (or Cancel, Option, Undo etc).

The revised version works the way I wanted:

import rhinoscriptsyntax as rs
import Rhino

gcodeFile = None
feedrate = 1500.0
spindle = 10000.0
retractHeight = 6.0
tool = 0
jobOrigin = 0
firstPath = True
gcodeState = dict(
	x = 0,
	y = 0,
	z = 0,
	f = 0
	)



# prompt for an output location and setup origin, and then repeatedly
# prompt for toolpaths and machine settings to write to the file in order
def event_loop():
	global gcodeFile
	global feedrate
	global spindle
	global retractHeight
	global tool
	global jobOrigin
	global firstPath
	pathList = []
	L = 0
	filename = rs.SaveFileName(
		title = "Export G Code",
		filter = "G Code File (*.nc)|*.nc|All files (*.*)|*.*||",
		filename = "Untitled",
		extension = "nc"
		)
	if not filename: return
	gcodeFile = open(filename, "w")
	jobOrigin = rs.GetPoint("Select toolpath origin")
	prompt = Rhino.Input.Custom.GetObject()
	feedrateOption = Rhino.Input.Custom.OptionDouble(feedrate, 1.0, 5000)
	spindleOption = Rhino.Input.Custom.OptionDouble(spindle, 0, 10000.0)
	retractOption = Rhino.Input.Custom.OptionDouble(retractHeight, 0, 100.0)
	toolOption = Rhino.Input.Custom.OptionInteger(tool, 0, 1000)
	prompt.EnablePreSelect(False, True)
	prompt.EnableUnselectObjectsOnExit(False)
	prompt.OneByOnePostSelect = True
	prompt.GeometryFilter = Rhino.DocObjects.ObjectType.Curve
	while True:
		prompt.ClearCommandOptions()
		prompt.AcceptNothing(True)
		prompt.AddOptionDouble("Feed", feedrateOption, "New feed rate (mm/min)")
		prompt.AddOptionDouble("Speed", spindleOption, "New spindle speed (rpm)")
		prompt.AddOptionDouble("Retract", retractOption, "Safe retract height (mm)")
		prompt.AddOptionInteger("Tool", toolOption, "Tool number")
		prompt.SetCommandPrompt(
			"Select first toolpath curve" if firstPath else "Select next toolpath"
			)
		rc = prompt.GetMultiple(L+1, L+1)
		prompt.EnableClearObjectsOnEntry(False)
		prompt.DeselectAllBeforePostSelect = False
		if prompt.CommandResult() != Rhino.Commands.Result.Success:
			return prompt.CommandResult()
		if rc == Rhino.Input.GetResult.Object:
			pathList = prompt.Objects()
			L = len(pathList)
			append_toolpath(pathList[L-1].Curve())
			continue
		elif rc == Rhino.Input.GetResult.Option:
			opt = prompt.OptionIndex()
			if feedrateOption.CurrentValue != feedrate:
				feedrate = feedrateOption.CurrentValue
			if spindleOption.CurrentValue != spindle:
				spindle = spindleOption.CurrentValue
				if not firstPath: gcodeFile.write("M3 S{}\n".format(gcs(spindle)))
			if retractHeight != retractOption.CurrentValue:
				retractHeight = retractOption.CurrentValue
			if tool != toolOption.CurrentValue:
				tool = toolOption.CurrentValue
				gcodeFile.write("M6 T{:d}\n".format(tool))
			continue
		elif rc == Rhino.Input.GetResult.Nothing:
			close_file()
			print ("Exported GCode to " + filename)
			break
		elif rc == Rhino.Input.GetResult.Cancel:
			#TODO use a temp file so it's actually possible to cancel export...
			close_file()
			print ("GCode Export cancelled")
		break

	return Rhino.Commands.Result.Success




#format numbers for CNC without wasting bytes
def gcs(num): return "{:.99g}".format(round(num,3))

#convert Rhino units to mm
def rtomm(n): return n * rs.UnitScale(2)

#convert mm to Rhino units
def mmtor(n): return n / rs.UnitScale(2)




# write out G code for initial setup and tool positioning
# (once we know where the first toolpath begins)
def write_prefix(startX=0, startY=0):
	global gcodeState
	gcodeFile.write("\n".join((
		"G21 (distances in mm)",
		"G90 (absolute coords)",
		"G17 (use XY plane for arcs)",
		"G0 Z" + gcs(retractHeight) + " (rapid to safe height)",
		"G0 X" + gcs(startX) + " Y" + gcs(startY) + " (rapid to start XY)",
		"M3 S" + gcs(spindle) + " (run spindle clockwise)",
		"",
		)))
	gcodeState['x'] = startX
	gcodeState['y'] = startY
	gcodeState['z'] = retractHeight
	return



# write out G code for a toolpath, prepending commands to
# move the tool safely (you hope) from its previous position
def append_toolpath(curve):
	global gcodeState
	global firstPath
	if rs.IsPolyline(curve):
		polyline = curve
	else:
		polyline = rs.ConvertCurveToPolyline(curve, 5.0, mmtor(0.01), False)
	points = rs.PolylineVertices(polyline)
	point = rs.PointSubtract(points[0], jobOrigin)
	if firstPath:
		write_prefix(point.X, point.Y)
		firstPath = False
	else:
		gcodeFile.write("G1 Z{}\nG0 X{} Y{}\n".format(
			gcs(retractHeight),
			gcs(point.X),
			gcs(point.Y)
			))
	gcodeFile.write("\n(toolpath {}mm)\n".format(gcs(rs.CurveLength(curve))))
	for i in range(len(points)):
		point = rs.PointSubtract(points[i], jobOrigin)
		write_G1(point)
	if not rs.IsPolyline(curve): rs.DeleteObject(polyline)
	return


# write out a G1 command (single linear move) in a compact form,
# setting the X,Y,Z and F parameters only where they have changed
def write_G1(dest):
	epsilon = mmtor(0.001)
	if rs.Distance(dest, (gcodeState['x'],gcodeState['y'],gcodeState['z'])) < epsilon:
		return
	cmd = "G1 "
	sep = ""
	if abs(dest.X - gcodeState['x']) > epsilon:
		cmd += "X" + gcs(rtomm(dest.X))
		gcodeState['x'] = dest.X
		sep = " "
	if abs(dest.Y - gcodeState['y']) > epsilon:
		cmd += sep + "Y" + gcs(rtomm(dest.Y))
		gcodeState['y'] = dest.Y
		sep = " "
	if abs(dest.Z - gcodeState['z']) > epsilon:
		cmd += sep + "Z" + gcs(rtomm(dest.Z))
		gcodeState['z'] = dest.Z
		sep = " "
	if feedrate != gcodeState['f']:
		cmd += sep + "F" + gcs(feedrate)
		gcodeState['f'] = feedrate
	gcodeFile.write(cmd + "\n")
	return



# move tool to a safe height, write final commands and close file
def close_file():
	gcodeFile.write ("G1 Z" + gcs(retractHeight) + "\n\n")
	gcodeFile.write ("M5 (stop spindle)\nM30 (program end)\n")
	gcodeFile.close()
	return



if(__name__ == "__main__"):
	event_loop()

gcExport.py (6.3 KB)

I don’t think it would be possible to figure this out from the documentation alone!

1 Like

I had a couple of further thoughts about this…

Here is why I was confused: I assumed that the ‘GetX()’ methods would let the user select some objects, and then return a result indicating what was just selected (like with rhinoscriptsyntax).

But what those methods actually do is simply allow the user to interact with Rhino (subject to certain constraints) and then return a result describing the last thing the user did. The selection may or may not have changed, and it may even contain fewer objects than you started with. Regardless of the result code, you have to look at GetObject.Objects() to know what is currently selected.

So I guess that makes sense. But my next move was going to be to allow the user to select multiple curves at a time, and it seems like there is no way I can do this and still have my script process objects as they are added, without the user pressing enter first. Although the FilletEdge command works like that, so there must be a way…

Are you looking for something like this?

import Rhino
from scriptcontext import doc
def getObjects():
    objects = []
    go = Rhino.Input.Custom.GetObject()
    go.OneByOnePostSelect = True
    go.SetCommandPrompt("Select objects")
    while True:
        go.Get()
        if go.CommandResult()!=Rhino.Commands.Result.Success:
            return go.CommandResult()
        object = go.Object(0).Object()
        object.Select(True)
        doc.Views.Redraw()
        objects.append(object)
        print "One {} added. {} objects are selected".format(object.ObjectType, len(objects))
        go.DeselectAllBeforePostSelect = False
        go.SetCommandPrompt("Select objects. Press Enter when done")

if __name__ == "__main__":
    getObjects()

getObjects.py (725 Bytes)

Thanks! That would have been an easier fix to my original question (I think the lack of doc.Views.Redraw() is why it didn’t work when I tried to select objects programmatically).

I notice that the behavior is slightly different to @clement’s script, where GetObject manages the selection. With your version, it doesn’t let me deselect (ctrl-click) previously selected objects during the Get() call. I assume this is because objects selected in code are not present in `GetObject.Objects()’.

If I want to get a return code each time the selection changes, though, it’s the same problem with either way – either I restrict the user to one object at a time, or the user has to press enter after each selection.

Hi @bobtato, imho your task just needs a better workflow UI wise. Instead of asking for curve objects one by one and letting the user define the properties per curve during the selection process, you might do this more efficient eg.using a Eto.Forms.GridView.

For each curve you would have individual rows with seperate columns for all values. This way you can select all in one go, then in the GridView allow to highlight curves, change order and values before writing the gcode to a file.

_
c.

@bobtato, note that i have no clue if this works on mac. It is basically what i wrote above except the order change. You could use your curve and option getter for the default values of course…

ETO_GridViewTest.py (5.7 KB)

_
c.