Python in DrawScript

This tutorial is intended to give you the skills necessary to:

  1. Execute Python code within a rule sequence or DrawScript that can dynamically change the program’s behavior

  2. Use evaluatable expressions in rule sequence and DrawScript rule parameters

  3. Save the results of a query to a Python variable

  4. Manipulate saved query results

To follow along, download the Complete DrawScript Template.
You can download the completed tutorial file here: Tutorial 7.3dm.

Warning

The Python integration within rule sequences and DrawScript is still very new. You’ll likely have to create a lot of functionality if you want something particularly advanced.

See Also

Part 1: Introducing DrawScript-Python

From it’s inception, DrawScript was designed to be a Turing-Complete programming language that bridges the computability gap in Shape Machine. Its visual nature allows for powerful, intuitive implementations of certain problems, but while geometric computation excels at certain tasks, it falls short for others. If one were able to leverage the strengths of both geometric and classical computation, the expressive power of DrawScript would be incredible. Enter: Python.

Shape Machine for Rhino is written in Python, and DrawScript is interpreted on demand by the Python program. The jump over to evaluating and executing Python code within DrawScript itself is therefore fairly trivial. This comes in two parts: dynamic execution and dynamic evaluation.

Dynamic Execution

Evaluating and executing Python code within DrawScript first requires that variables can be made, utilities can be imported, and functions can be defined, each for use across the entire DrawScript program. This is facilitated by providing the program with a context, a mapping from variable/function/etc. names to values. When something is imported or defined, it updates the context, and the updated context is later used when evaluating or executing Python code later in the program.

Dynamic execution is performed with the introduction of a new Python rule created for DrawScript. The only thing present in the Python rule is a Text element in the python layer. The text inside is any Python code you would like. When Shape Machine runes the Python rule, it executes this code using the builtin exec(), providing the context as the globals parameter.

Glossing over the nitty-gritty details, the important thing to know is that you can do whatever you’d like in a Python rule and take advantage of the side effects later within your DrawScript program.

Dynamic Evaluation

DrawScript has always been dynamically evaluated, but prior to DrawScript-Python, the parameters for each rule were evaluated at compile-time. With the introduction of DrawScript-Python, parameters are now evaluated dynamically, right before the rest of the rule is evaluated. More specifically, these parameters are evaluated as Python expressions.

This evaluation is performed using the same global context used for dynamic execution of Python rules. This means that you could create a variable, say x, in a Python rule, and use it in the Loop parameter of one of your DrawScript rules. The parameters are reevaluated every time Shape Machine returns to the rule, which means if you change x, the second time Shape Machine executes the rule, it will loop a different amount of times.

../_images/example-exec-and-eval.png

Example of dynamic expression followed by dynamic evaluation. Above the line is the bottom of a Python rule that sets a variable x to 10. In the subsequent rule, the Loop parameter is set to x. This has the effect of setting Loop=10 once evaluated.

Warning

Parameter evaluation is performed every time a rule loops, but the Loop parameter is evaluated only once when Shape Machine reaches a rule, in order to determine how many times to loop. This means that, for example, if you use the random module to randomize the Angle parameter, it will be different every iteration of the loop, but randomizing the Loop parameter will not cause the number of loops to change in each iteration of the loop.

Implications

Dynamic execution and evaluation of Python code in DrawScript has powerful implications. The following is a by-no-means-exhaustive list of things you can now do with DrawScript-Python:

  • Dynamically compute any number and use it in the Scale or Angle parameters

    • If you use this with a replacement rule with a single point in the query, you can use these parameters to set the output scale and angle to something you compute with Python.

  • Use ternary statements (x if statement else y) in the Loop parameter to easily control which rules get executed

  • Execute a preamble Python rule at the start of the DrawScript program

    • A preamble rule is one that precedes every other rule in the program, allowing you to define functions and variables that will be used throughout the entire program.

  • Use a preamble rule to provide the user of your program with an easy way to configure parameters of the program

  • Create your own popups that can either provide information or request information during the execution of your program

  • Take advantage of any Python library

The possibilities are as extensive as your imagination!

Part 2: Exercises

Exercise 1: Hello, DrawScript-Python!

In this exercise, we’ll use the classic example of printing a string to understand how to edit DrawScript-Python scripts. The scaffold for this exercise provided in the tutorial Rhino file already has a statement that prints to the console. We will simply be modifying that statement.

Step-by-Step Guide

  1. Modify the Provided DrawScript-Python

    • Change the content of the print statement to output “Hello, DrawScript-Python!”

Key Points

  • If you delete all the text in a Text element and leave the editor, you will have to create a new Text element in the python layer.

  • The font family, size, style, etc. of DrawScript-Python blocks can be whatever you want.

  • Print statements output to the command history bar.

Exercise 2: Randomly-Nested Squares

In this exercise, we’ll return to the nested squares, this time using a randomized loop count to determine how many times to nest them.

Step-by-Step Guide

  1. Import the random Module

    • Paste the following code into the Text element in the first Python rule:

from random import randint

x = randint(1, 6)
  1. Create the Nesting Rule

    • Search for a black square under similarity, replacing it with a green square with a black square nested inside.

    • Set the Loop parameter to x

      • Of course, because the Loop parameter is evaluated dynamically, you could set it directly to randint(1, 6).

  2. Convert All Green Lines to Black Lines

    • Search for green lines under similarity, replacing all of them with black lines.

Key Points

  • You can use variables and expressions within DrawScript rule parameters.

    • Anything you use has to be defined either in a Python rule or included by default.

  • Setting a variable to a random number lets it be used multiple times, each time providing the same number. If you instead call a random number function inside of your parameters, the value provided to the parameter will be different every evaluation.

Example: Boolean-Controlled Parameters

In this example, we’ll see how ternary expressions can be used in the Loop parameter to create highly controlled behavior.

This example is composed of 3 rules: one Python rule and two replacement rules. The first replacement rule simply colors a black point pink, the other turns it blue. Which one executes is determined by a variable defined in the Python rule: turnPink.

Looking at the Loop parameter in the first rule, we can see it says 1 if turnPink else 0. The second rule has the opposite: 1 if not turnPink else 0. The text here can visually extend beyond the boundary of the DrawScript block, but as long as the centerpoint of the Text element is within the boundary, this is okay.

Try running the DrawScript with turnPink = True in the Python rule, then with turnPink = False.

Key Points

  • Booleans can provide highly granular control over your program.

  • Ternary expressions can be used in any parameter, allowing you to control loop counts, transformation types, selection modes, etc.

  • Python rules can be used to define boolean variables. You could use this in tandem with jump rules to ensure that only one jump in a set of jumps is taken.

Example: Requesting Input From a User

This example shows two ways to request input from a user. It’s based on nested squares, but the user is asked to specify how many times to loop the nesting.

Method 1: Command Prompt

The first way to request feedback is using the command prompt within Rhino. To test this method, set popup = False in the first Python rule of this program.

Note

Input of this variety can be accessed either by using methods provided under the communication_layer variable (definition) or through RhinoScriptSyntax.

Source Code
x = communication_layer.get_user_int(
  "How many times should the nesting be looped?",
  1,
  1,
  10,
)

Method 2: Popup

The second way to request feedback is with popups. To test this method, set popup = True in the first Python rule of this program.

Note

If you’re interested in learning more about creating popups, Rhino uses the Eto library for this.

The goal of this example is not to teach you how to use this (it’s kind of a pain), it is instead simply to encourage creativity.

Source Code
import Eto.Drawing as drawing
import Eto.Forms as forms
import Rhino
import System

x = 0

def do_popup():
  global x
  popup = forms.Dialog()
  popup.BackgroundColor = drawing.Color(
    0.7450980392, 0.7450980392, 0.7450980392, 1
  )
  popup.Title = "Enter Loop Count"
  popup.Padding = drawing.Padding(5)
  popup.Resizable = False
  popup.Topmost = True
  popup.Maximizable = False
  popup.Minimizable = False
  popup.WindowStyle = forms.WindowStyle.NONE
  popup.MovableByWindowBackground = True
  popup.ShowInTaskbar = True

  description = forms.Label()
  description.Text = "Please enter a number to indicate how many times the nesting operation should loop."
  header = forms.Label()
  header.Text = "How Many Times Should Nesting Loop?"
  header.Font = drawing.Font(
    description.Font.Family,
    description.Font.Size + 4,
    drawing.FontStyle.Bold,
  )

  input_label = forms.Label()
  input_label.Text = "Loop count:"
  input = forms.TextBox()
  input.PlaceholderText = "Enter a number"

  ok_button = forms.Button()
  ok_button.Text = "Ok"
  ok_button.Click += lambda *args, **kwargs: popup.Close()

  layout = forms.DynamicLayout()
  layout.Spacing = drawing.Size(10, 10)
  layout.AddRow(header)
  layout.AddRow(description)
  layout.AddRow(None)
  layout.AddRow(input_label, input)
  layout.AddRow(ok_button)

  popup.Content = layout

  popup.ShowModal()

  x = int(input.Text)

# Forms need to be built and shown on the UI thread, but DrawScript runs on an
#  asynchronous thread. This means we need to do the following to keep the threads
#  happy.
Rhino.RhinoApp.InvokeAndWait(System.Action(do_popup))

Part 3: Saving Queries to Variables

In addition to executable Python rules, DrawScript-Python also supports the ability of saving shape queries to a variable. With a query result saved in a variable, you can do whatever you want with geometry the Shape Machine finds within your designs.

This behavior is facilitated with save-search rules. The LHS is identical to what you’d have in a replacement rule, instructing Shape Machine what to search for. The RHS is a single Text element that allows you to specify the name of the variable to save the search results to.

../_images/blank-save-search.png

The results of the search will be provided in one of two ways. If selection mode 1 is used (or 3 followed by selecting a single match), the results will be saved as a single shapemachine.geometry.shape.Shape. If, instead, selection mode 2 is used (or 3 followed by selecting all matches), the results will be saved as a list of shapemachine.geometry.shape.Shapes. If no matches are found, the results will either be saved as an empty shapemachine.geometry.shape.Shape or an empty list, based on the selection mode.

Warning

Taking advantage of save-search rules requires familiarity with Shape Machine’s Python API, found here.

There will be no dedicated exercises taking advantage of the save-search rules, but the following example shows a very simple use case that you can imagine could be extended further.

Example: Finding the Closest Two Points to Another Point

With save-search rules, you can find one point, then find the two points that are closest to it. The two closest points are changed to be red. It’s a fairly mundane example, but you can hopefully imagine how this sort of thing might be useful, perhaps even in tandem with mark-intersection rules.

Play around with the example, move the points around, and see how the program behaves.

Source Code
import numpy as np
from shapemachine.geometry.shape import Shape
from shapemachine.geometry.point import Point

# In this case, target is a Shape, and points is a list of Shapes.
#  as such, we have access to the center attribute. Because these shapes
#  are all single points, this, in effect, works to get the location of the point.

def distance_to_target(shape):
  return np.linalg.norm(target.center - shape.center)

# Sort the list of points by proximity to the target point
sorted_points = sorted(points, key=distance_to_target)

# Unpack the closest two points
closest, second_closest = sorted_points[:2]
# Get the attributes of the Red layer
red_attr = make_attributes("Red")

# The remaining steps simply replace the two closest black points
#  with red versions, keeping them usable for the engine in the
#  final replacement rule.
# This is done in two parts: updating the engine's current design,
#  which allows Shape Machine to use the geometry later, and replacing
#  the current design in the communication layer, which updates what you
#  see in Rhino.
engine.current_design -= closest
engine.current_design -= second_closest
engine.current_design += Shape(
  [],
  [],
  [
    Point(closest.center, red_attr),
    Point(second_closest.center, red_attr)
  ]
)
communication_layer.replace_current_design(engine.current_design, engine.to_design)\

Part 4: Conclusion

In this tutorial, you learned:

  • Python Rules: A new type of rule in DrawScript that allows you to dynamically execute Python code within a DrawScript program.

  • Dynamic Parameter Evaluation: DrawScript parameters are evaluated dynamically each time Shape Machine executes the rule. The Loop parameter is only evaluated once per rule per block.

  • Save-Search Rule: A new type of rule in DrawScript that allows you to search for a shape and save the search result(s) to a variable, for use in a Python rule or parameter evaluation.

  • Additional Resources: Access additional resources for DrawScript-Python via the dedicated page: DrawScript-Python Reference