Display Conduit in Python 3

Hi all!

In the past, I’ve used the Rhino.Display.DisplayConduit class in C# to draw objects in the Rhino viewport without adding them to the doc. Now I am trying to do something similar inside of a Python 3 component in Rhino 8. Basically, I would like to render objects to the Rhino Viewport inside of a Python loop without having to output them from the component. Is this possible? Is there something in rhinoscriptsyntax that gives you access to the display pipeline?

@eirannejad, I’m sure you’ve got some tricks up your sleeve :slight_smile:

Thanks!
SP

Sure. See this example made by @pascal

test_noisifyCurves.py (6.9 KB)

1 Like

Amazing, thank you! I didn’t know you could do this in Python, but… makes sense.

I tried running the code as is inside the Python 3 component in Grasshopper and I am getting the following error:

System.NotImplementedException: The method or operation is not implemented.
   at Rhino.Runtime.Code.Languages.PythonNet.CPythonCode.Execute(RunContext context)
   at Rhino.Runtime.Code.Code.ExecTry(RunContext context, IPlatformDocument& doc, Object& docState)
   at Rhino.Runtime.Code.Code.Run(RunContext context)

However, this may or may not be relevant to what I am trying to do, just posting it here in case you know what’s going on. I’ll try implementing it in my code. Thank you so much!

1 Like

@enmerk4r it would be great if you can share a grasshopper example so I see if I can replicate. Thanks!

I wrote a simple script that generates a bunch of random spheres in a loop and tries to render them to screen. I run the following code on each iteration of the loop:

conduit = DrawMeshesConduit(meshes)

conduit.Enabled = True
sc.doc.Views.Redraw()

time.sleep(0.25)

conduit.Enabled = False
sc.doc.Views.Redraw()

However, I don’t seem to be hitting CalculateBoundingBox or DrawOverlay. I must be missing something super obvious.

preview-random-spheres.gh (9.3 KB)

you are not getting the calls to CalculateBoundingBox and DrawOverlay because they’re are defined inside of your __init__ method.

Bad indentation:

class DrawMeshesConduit(Rhino.Display.DisplayConduit):
    def __init__(self, meshes):
        super().__init__()

        ...

        def CalculateBoundingBox(self, e):
            ...
    
        def DrawOverlay(self, e):
            ...

Correct indentation:

class DrawMeshesConduit(Rhino.Display.DisplayConduit):
    def __init__(self, meshes):
        super().__init__()

        ...

        def CalculateBoundingBox(self, e):
            ...
    
        def DrawOverlay(self, e):
            ...

What is this grasshopper definition need to do? Grasshopper has its own display conduit for preview and if you implement a GH_ScriptInstance you can use the method overrides to draw your preview code:

import Rhino
import Rhino.Geometry as RG
import Grasshopper
import System.Drawing as SD

import rhinoscriptsyntax as rs


class MyComponent(Grasshopper.Kernel.GH_ScriptInstance):
    def RunScript(self, x, y):
        self.center = RG.Point3d(5, 5, 5)
        self.scale = y
        return Rhino.Geometry.Sphere(self.center, x)

    # Preview overrides
    @property
    def ClippingBox(self):
        return RG.BoundingBox(RG.Point3d(0, 0, 0), RG.Point3d(5, 5, 5))

    def DrawViewportWires(self, args):
        args.Display.Draw2dLine(SD.Point(0, 0), SD.Point(100, 100), SD.Color.Red, 5)

    def DrawViewportMeshes(self, args):
        args.Display.DrawDirectionArrow(
            self.center, RG.Vector3f(self.scale, 0.5, 0), SD.Color.Blue
        )
        args.Display.DrawDirectionArrow(
            self.center, RG.Vector3f(0.5, self.scale, 0), SD.Color.Red
        )
        args.Display.DrawDirectionArrow(
            self.center, RG.Vector3f(0, 1, self.scale), SD.Color.Green
        )

        args.Display.Draw2dText(
            "Example Text", SD.Color.Yellow, RG.Point2d(30, 30), False, 16, "Consolas"
        )

These buttons can help you setup the class implementation:

See this example:

test_gh_display.gh (6.6 KB)

Thanks! I fixed the indentations, but, for some reason, still not hitting those methods:

preview-random-spheres-fixed-indentation.gh (9.2 KB)

Here’s what I’m trying to do. Do you remember our conversation from back when R8 was a WIP, where I expressed interest in experimenting with reinforcement learning in Rhino? Back then the new ScriptEditor had several issues that prevented me from using Pandas and StableBaselines3. But now it’s a lot more stable and I was finally able to implement a simple Gridworld example in OpenAI gym.

The training process is iterative by nature: you have multiple training “episodes” and each “episode” consists of a series of “steps” that represent an environment state at that specific point in time. OpenAI gym lets you define a render(...) callback to draw a current step in whichever way you choose. I am trying to make the render(...) callback commit meshes and curves to Rhino’s display pipeline so that I can get a live preview of the training process in the viewport.

Basically, I need a way of rendering multiple frames (steps) within a single iteration of SolveInstance (or RunScript I guess). And yes, I can always dump environment states as JSON to disc and then visualize everything once the training is complete, but what’s the fun in that? :grin:

Here’s my basic Gridworld example:

Gridworlds_2.gh (9.0 KB)

The Solve step is completely different from Draw step. This is how Grasshopper (and for what it is worth any other application that computes and draws e.g. game engines) work.

Grasshopper 1 is not asynchronous therefore Solve step can not halt the execution of the main thread, that also draws the rest of the UI. During the solve you can only Invalidate the UI. The redrawing of the UI actually happens when you have returned the control from the Solve call.

Here is what I would suggest:

  • Run the solve on a non-ui thread.
  • On every solve, update current mesh on the script instance,
  • Use a GH trigger to call a recompute / redraw on the component every 500ms or so

TrainingExample.gh (6.2 KB)

import System
import System.Drawing as SD
import Rhino
import Rhino.Geometry as G
import Grasshopper
import Grasshopper.Kernel as GHK
import threading
import time


def main_solve():
    for r in range(10, 20):
        # wait represents compute work
        Rhino.RhinoApp.WriteLine("computing mesh")
        time.sleep(1)

        sphere = G.Sphere(G.Point3d.Origin, r)
        MyComponent.CURRENT_MESH = G.Mesh.CreateFromSphere(sphere, 10, 10)
        Rhino.RhinoApp.WriteLine("computed mesh")

    Rhino.RhinoApp.WriteLine("computed completed")


class MyComponent(Grasshopper.Kernel.GH_ScriptInstance):
    SOVLE_STARTED = False
    CURRENT_MESH = None

    def RunScript(self):
        # outputing a point to get gh to call draw methods
        if MyComponent.SOVLE_STARTED:
            return ("Training in Progress", G.Point3d.Origin)
        
        MyComponent.SOVLE_STARTED = True
        threading.Thread(target=main_solve).start()
        return ("Training in Progress", G.Point3d.Origin)
        
    @property
    def ClippingBox(self):
        return G.BoundingBox(-25, -25, -25, 25, 25, 25)

    def DrawViewportMeshes(self, args: GHK.GH_PreviewMeshArgs):
        d = args.Display
        if MyComponent.CURRENT_MESH:
            d.DrawMeshWires(MyComponent.CURRENT_MESH, SD.Color.Red, 2)

Now this could be executed from the main script editor as well. Not sure if you want this in GH but I made the example in Grasshopper. I can make a standalone editor example as well if you want.

1 Like

Fantastic! Thank you! This is exactly what I needed :slight_smile:

1 Like