Rhino / Grasshopper
This guide provides the basic information about our Rhino/Grasshopper integration.
If you setup this integration for the first time, we recommend to follow the Rhino/Grasshopper tutorial.
There are 2 ways to integrate VIKTOR with Rhino/Grasshopper:
- Connect to Rhino/Grasshopper on your local computer via the Hops component and a Generic worker
- Connect to a Rhino Compute Server
We recommend to use the worker (1) when you don't have a Rhino Compute server yet and you want to get started quickly, and to setup a Rhino Compute server (2) when you want to go to production with your app.
System requirements:
- 💻 Windows PC
- 🦏 Rhino 7 with Hops plugin installed
- 🐍 Python =< 3.10
Method 1: Connect to Rhino/Grasshopper on your local computer
Grasshopper scripts can be executed straight from Python, by using the Hops plugin for Rhino. This makes a direct and fast connection possible from a VIKTOR application to any Grasshopper script.
VIKTOR's Rhino / Grasshopper integration requires a Generic worker.
VIKTOR app
In the VIKTOR app, the input and output files for the worker are defined and the GenericAnalysis
is executed to run the Generic worker on the server:
from io import BytesIO
import json
from viktor.external.generic import GenericAnalysis
from viktor.views import GeometryView
from viktor.views import GeometryResult
@GeometryView("Geometry", duration_guess=10, update_label='Run Grasshopper')
def run_grasshopper(self, params, **kwargs):
# Create a JSON file from the input parameters
input_json = json.dumps(params)
# Generate the input files
files = [('input.json', BytesIO(bytes(input_json, 'utf8')))]
# Run the Grasshopper analysis and obtain the output files
generic_analysis = GenericAnalysis(files=files, executable_key="run_grasshopper", output_filenames=["geometry.3dm"])
generic_analysis.execute(timeout=60)
threedm_file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
return GeometryResult(geometry=threedm_file, geometry_type="3dm")
Worker configuration - config.yaml
The executable_key in the example above refers to the "run_grasshopper" command. This command should also be specified in the configuration file on the server, located in the same directory as the worker:
executables:
run_grasshopper:
path: 'C:\Users\your-name\AppData\Local\Programs\Python\Python310\python.exe'
arguments:
- 'C:\Users\your-name\viktor-grasshopper\run_grasshopper.py'
workingDirectoryPath: 'C:\Users\your-name\viktor-grasshopper'
maxParallelProcesses: 1 # must be 1
In this example when a job is received, the worker will first place the input.json file in the working directory, and then run the executable "run_grasshopper". When the job is completed, the geometry.3dm file will be sent back to the VIKTOR app.
Grasshopper script - script.gh
The Grasshopper script should be defined so that the Hops 'Get components' are connected to the inputs and outputs. The 'Get components' can be found under Params Tab > Util Group. Components preceded by 'Get' can be used as an input parameter. The 'Context bake' and 'Context print' components should connected to the desired output components to return geometry or text respectively. See the Rhino documentation for an extensive explanation.
Python script run_grasshopper.py
The Grasshopper script can be executed from a Python script on the same server. For the above Grasshopper script, the corresponding Python file would look like the file below. Place the run_grasshopper.py
file in the same folder of where the input and output files are located. It is not necessary to keep the script.gh file open during execution, but in order for Hops to listen to requests from the Python script, both Rhino and Grasshopper need to be running !
# Pip install required packages
import os
import json
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm
# Set the compute_rhino3d.Util.url, default URL is http://localhost:6500/
compute_rhino3d.Util.url = 'http://localhost:6500/'
# Define path to local working directory
workdir = os.getcwd() + '\\'
# Read input parameters from JSON file
with open(workdir + 'input.json') as f:
input_params = json.load(f)
# Create the input DataTree
input_trees = []
for key, value in input_params.items():
tree = gh.DataTree(key)
tree.Append([{0}], [str(value)])
input_trees.append(tree)
# Evaluate the Grasshopper definition
output = gh.EvaluateDefinition(
workdir + 'sample_box_grasshopper.gh',
input_trees
)
# Create a new rhino3dm file and add resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = output['values'][0]['InnerTree']['{0}'][0]['data']
obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)
# Save the rhino3dm file to your working directory
file.Write(workdir + 'geometry.3dm', 7)
Method 2: Connect to a Rhino Compute Server
Below you find a short explanation of the setup, an extensive guide can be found here.
Using this method you can directly connect to a Rhino Compute Server from the VIKTOR app code. Once you have the Rhino Compute Server running, you should have a URL
and an API key
, to which you can refer in your VIKTOR app:
import os
from pathlib import Path
import json
import tempfile
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm
from viktor.views import GeometryView
from viktor.views import GeometryResult
@GeometryView("Geometry", duration_guess=1, update_label='Run Grasshopper')
def run_grasshopper(self, params, **kwargs):
# Credentials for Rhino Compute api
compute_rhino3d.Util.url = os.getenv("RHINO_COMPUTE_URL")
compute_rhino3d.Util.apiKey = os.getenv("RHINO_COMPUTE_API_KEY")
# Create an input tree with the input parameters
input_trees = []
for key, value in params.items():
tree = gh.DataTree(key)
tree.Append([{0}], [str(value)])
input_trees.append(tree)
# Evaluate the Grasshopper definition
output = gh.EvaluateDefinition(
str(Path(__file__).parent / 'sample_box_grasshopper.gh'),
input_trees
)
# Create a new rhino3dm file and add resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = output['values'][0]['InnerTree']['{0}'][0]['data']
obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)
# Write a temporary file
temp_file = tempfile.NamedTemporaryFile(suffix=".3dm", delete=False, mode="wb")
temp_file.close()
file.Write(temp_file.name, 7)
return GeometryResult(geometry=File.from_path(Path(temp_file.name)), geometry_type="3dm")
When you start the app, add the credentials as follows:
> viktor-cli start --env RHINO_COMPUTE_URL="YOUR_URL" --env RHINO_COMPUTE_API_KEY="YOUR_API_KEY"
When you publish your app, add the environment variables for the URL and API key to the app as described in this guide.
Testing
mock_GenericAnalysis
decorator for easier testing of GenericAnalysis
GenericAnalysis.execute
needs to be mocked within
the context of (automated) testing.
The viktor.testing
module provides the mock_GenericAnalysis
decorator that facilitate mocking of workers:
import unittest
from viktor import File
from viktor.testing import mock_GenericAnalysis
from app.my_entity_type.controller import MyEntityTypeController
class TestMyEntityTypeController(unittest.TestCase):
@mock_GenericAnalysis(get_output_file={
'result.xml': File.from_path('test_file.xml'), # <name>: <File>
'result.json': File.from_path('test_file.json'),
...
})
def test_generic_analysis(self):
MyEntityTypeController().generic_analysis()
For the decorator's input parameters the following holds:
- If a Sequence type is provided, the next entry is returned for each corresponding method call. When a call is performed on a depleted iterable, an Exception is raised.
- If a single object is provided, the object is returned each time the corresponding method is called (endlessly).
- If None is provided (default), a default File/BytesIO object (with empty content) is returned each time the corresponding method is called (endlessly).