Skip to main content

Rhino / Grasshopper

tip

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.

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.

System requirements:

  • 💻 Windows PC
  • 🦏 Rhino 7 with Hops plugin installed
  • 🐍 Python =< 3.10

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)

Testing

New in v13.5.0

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).