Python 2 -> Python 3 - Custom Curve Highlight - Draw Foreground

Hello,

I’m updating some of my older scripts and having an issue in Python 3 related to the following lines, I have a single input value in an input “T” for “Thickness”, the input Type is an Int with List Access.

This code works fine in the old script component. Is this a Python2 → Python3 syntax thing or related to the script editor? @eirannejad

The intent is to draw curves with a custom lineweight and thickness to the foreground of the viewport.

               thickness = self.t[i % len(self.t)] if self.t else None

Error:

1. Error running script: 'int' object is not subscriptable

Full Code:

from ghpythonlib.componentbase import executingcomponent as component
import Rhino
import System

__author__ = "Michael Vollrath"
__version__ = "2023.08.01"
# Made With <3 In Dallas, TX
# Modification of script originally created by Seghier Khaled

ghenv.Component.Name = "Custom Preview Lineweight"
ghenv.Component.NickName = "CPL"
ghenv.Component.Description = "Custom Preview component allowing thickness and color preview of curves"

ghenv.Component.Params.Input[0].Name = "Curve"
ghenv.Component.Params.Input[0].NickName = "C"
ghenv.Component.Params.Input[0].Description = "Input Curves"

ghenv.Component.Params.Input[1].Name = "Thickness"
ghenv.Component.Params.Input[1].NickName = "T"
ghenv.Component.Params.Input[1].Description = "Curve Thickness"

ghenv.Component.Params.Input[2].Name = "Color"
ghenv.Component.Params.Input[2].NickName = "Pc"
ghenv.Component.Params.Input[2].Description = "Preview Color"


class CurveColor(component):

    def RunScript(self, C, T, Pc):
        self.C = C
        self.RGB = Pc
        self.t = T
        if not self.C:
            return

    def DrawViewportWires(self, arg):
        if not self.C:
            return

        # Check if self.C is a list
        if isinstance(self.C, list):
            for i, curve in enumerate(self.C):
                # Get the corresponding thickness and RGB values using modulo to wrap the lists
                thickness = self.t[i % len(self.t)] if self.t else None
                rgb = self.RGB[i % len(self.RGB)] if self.RGB else None
                self.DrawCurveWithColor(arg, curve, thickness, rgb)
        else:
            # Single curve case
            self.DrawCurveWithColor(arg, self.C, self.t[0] if self.t else None, self.RGB[0] if self.RGB else None)

    def DrawCurveWithColor(self, arg, curve, thickness=None, rgb=None):
        if not curve:
            return
        if thickness is None:
            thickness = self.t
        if rgb is None:
            rgb = self.RGB
        arg.Display.DrawCurve(curve, rgb, thickness)

    # Handle Draw Foreground Events
    def __exit__(self):
        Rhino.Display.DisplayPipeline.DrawForeground -= self.DrawViewportWires

    def __enter__(self):
        Rhino.Display.DisplayPipeline.DrawForeground += self.DrawViewportWires

@AndersDeleuran have you come across this by chance?

Thank you all for your response!

1 Like

I’m afraid I can’t reproduce the error, but I’m seeing some pretty odd behaviour otherwise:


240319_DrawStuff_CPython_00.gh (7.0 KB)

1 Like

I’ve used a c# script for curve display in Rhino 7 but now I’m Rhino 8 I switched to the native curve display component. I like how I can set linetype styles in Rhino or with the new Grasshopper Rhino components…

1 Like

I noticed that as well… when I switch between List or Item access on the script component input parameters, it defines them as System.Object Generic List which makes sense to my limited understanding of how a list is actually defined under the hood but it throws errors and complains then…

I’m confused if it’s the script component or the code.

I did as well for most all use cases, I love it. However, for this script I’m specifically after the ability to have the curves drawn to the foreground to act as a “curve highlighter” that I can set the color and color alpha on of course.

Here’s what it should look like:

1 Like

What’s connected to T, presumably an Int? If so, is T set to item access instead of list access?


class CurveColor(component):

    def RunScript(self, C, T, Pc):

        self.t = T
                thickness = self.t[i % len(self.t)] if self.t else None

I didn’t know those context manager dunder methods could be overridden by the way. Does Grasshopper instantiate mycomponents like that?

Hi @James_Parrott, T is an Int, yes. Currently it is set by right clicking the Parameter input and setting it there.

Current value is 10.

I actually don’t know. This is code Ive modified from other sources but I didn’t write or know how to write this myself off the bat so I’m not the best person to be able to answer that unfortunately.

Great. Either the input access type on the component input for T should be set to “list”, or if T really should be an Int, then the code should be rewritten to avoid trying to subscript it in self.t[...] after self.t = T (but the intention from the code very much looks like indexing into a list).

The error is probably exactly the same, as if you tried to do: 4['e']

If really needed, a patch is also possible such as: self.t = [T] if isinstance(T, int) else T.

Okay after going down the Display Conduit route I got a solution working and then realized I couldn’t figure out how to remove it dynamically and then came back around to this similar solution from you @AndersDeleuran

I adapted it and got it working how I want, thank you! Any idea how to get this working in Python3 though? I know @eirannejad removed the need for the SDK/override or whatever it was called but in the Python3 version of this it doesn’t recognize component as valid when imported as follows:

  1. Error begin run: No module named ‘GhPython.Assemblies.ExecutingComponent’ [4:1]

I’m unsure what the Python3 version would be?

Model Space:

Graph Space:

Code:

import System
import Rhino as rc
import Grasshopper as gh
import GhPython.Assemblies.ExecutingComponent as component

class MyComponent(component):

    def RunScript(self, Curves, Thickness, Colors):

        # Filter out null curves
        non_null_curves = [curve for curve in Curves if curve is not None]

        # Ensure all input lists have the same length
        max_length = max(len(non_null_curves), len(Thickness), len(Colors))
        self.cols = Colors * (max_length // len(Colors)) + Colors[:max_length % len(Colors)] if Colors else [System.Drawing.Color.OrangeRed] * max_length
        self.black = System.Drawing.Color.Black
        self.thickness = Thickness * (max_length // len(Thickness)) + Thickness[:max_length % len(Thickness)] if Thickness else [1.0] * max_length
        self.curves = non_null_curves

    def DrawForeground(self, sender, arg):

        if (not ghenv.Component.Hidden and not ghenv.Component.Locked 
            and ghenv.Component.OnPingDocument() == gh.Instances.ActiveCanvas.Document
            and arg.Viewport.Id == arg.RhinoDoc.ActiveDoc.Views.ActiveView.ActiveViewportID):

            # Draw Stuff To Foreground
            for i in range(len(self.curves)):
                # Draw input Curve(s)
                arg.Display.DrawCurve(self.curves[i], self.cols[i], self.thickness[i])

    def __enter__(self):
        rc.Display.DisplayPipeline.DrawForeground += self.DrawForeground

    def __exit__(self):
        rc.Display.DisplayPipeline.DrawForeground -= self.DrawForeground

Having some fun testing it:

Graph Space:

2 Likes

I’m afraid I’ve put the new script editor on hold for now, at least till it becomes more stable and has feature parity with GHPython. Staying tuned though :eyes:

1 Like

@michaelvollrath If you set an input parameter to List, the component will show the type hinting in front of the parameter:

    def RunScript(self, T: System.Collections.Generic.IList[int]):

Not sure why the example you shared does not have this type hinting.

Let me know if you can replicate this.

Just to make sure we’re seeing the same thing: In the video I posted above, the error is only raised when the curve parameter that is input to the C parameter on the script editor component is set to preview its geometry. When I turn the preview off, the error is not thrown (but also nothing else happens). This is quite odd behaviour and not something I’ve ever seen with the GHPython component (ping @eirannejad).

Hi @eirannejad , it was auto populating this as you mention. I was getting some errors with it (perhaps not from this but I thought it was) so I manually deleted the type hinting as I was testing and got it working.

I’ll see if I can reproduce the issue I was having.

1 Like

@AndersDeleuran I was able to get the code working by changing this line:

import GhPython.Assemblies.ExecutingComponent as component

To this:

import ghpythonlib.component as component

Here’s the rest of the code, now working for Python 3, not sure if this would work for similar scripts you produced? May be worth checking out though so thought I’d share.

EDIT: Updated the code below with a little bit better error handling…

Code:

import System
import Rhino
import Grasshopper as gh
import ghpythonlib.component as component  # Changed to this for Python 3

__author__ = "Michael Vollrath"
__version__ = "2024.03.20"
# Made With <3 In Dallas, TX
# Modification of script originally created by Seghier Khaled

ghenv.Component.Name = "Curve Highlight"
ghenv.Component.NickName = "CH"
ghenv.Component.Description = "Custom Preview component allowing thickness and color preview of curves drawn in the foreground"

ghenv.Component.Params.Input[0].Name = "Curve"
ghenv.Component.Params.Input[0].NickName = "C"
ghenv.Component.Params.Input[0].Description = "Input Curves"

ghenv.Component.Params.Input[1].Name = "T"
ghenv.Component.Params.Input[1].NickName = "T"
ghenv.Component.Params.Input[1].Description = "Curve Thickness"

ghenv.Component.Params.Input[2].Name = "Color"
ghenv.Component.Params.Input[2].NickName = "Pc"
ghenv.Component.Params.Input[2].Description = "Preview Color"


class MyComponent(component):

    def RunScript(self,
            C: System.Collections.Generic.IList[Rhino.Geometry.Curve],
            T: System.Collections.Generic.IList[int],
            Pc: System.Collections.Generic.IList[System.Drawing.Color]):

        msg_level = ghenv.Component.RuntimeMessageLevel.Warning

        # Add warning message
        if not C or len(C) == 0:
            ghenv.Component.AddRuntimeMessage(msg_level, "Input C Failed To Collect Data")
            non_null_curves = []
        else:
            # Filter out null curves
            non_null_curves = [curve for curve in C if curve is not None]

        # Set default value for thickness if T is None or empty
        if not T or len(T) == 0:
            T = [10.0]

        # Set default value for color if Pc is None or invalid
        if not Pc or any(color is None for color in Pc):
            Pc = [System.Drawing.Color.OrangeRed]

        # Ensure all input lists have the same length
        max_length = max(len(non_null_curves), len(T), len(Pc))
        # Repeat the elements in the list to match the max_length
        self.cols = [Pc[i % len(Pc)] for i in range(max_length)]
        self.thickness = [T[i % len(T)] for i in range(max_length)]
        self.black = System.Drawing.Color.Black
        self.curves = non_null_curves

    def DrawForeground(self, sender, arg):

        if (not ghenv.Component.Hidden and not ghenv.Component.Locked
            and ghenv.Component.OnPingDocument() == gh.Instances.ActiveCanvas.Document
            and arg.Viewport.Id == arg.RhinoDoc.ActiveDoc.Views.ActiveView.ActiveViewportID):

            # Draw Stuff To Foreground
            for i in range(len(self.curves)):
                # Draw input Curve(s)
                arg.Display.DrawCurve(self.curves[i], self.cols[i], self.thickness[i])

    def __enter__(self):
        Rhino.Display.DisplayPipeline.DrawForeground += self.DrawForeground

    def __exit__(self):
        Rhino.Display.DisplayPipeline.DrawForeground -= self.DrawForeground

Example Use Case:

Graph Space:

Model Space:

2 Likes

Ah that’s good to hear, thanks for the update. I’m running a workaround about all this on a month and it’d be nice to have these methods working in Rhino 8. The behaviour I reported above is quite worrying/baffling though, so think I’ll stick with Rhino 7 as the primary platform for the workshop.

Edit: Just had a closer look at the code. It looks like it’s implementing the pattern from over here, it’s great to see that working in CPython :raised_hands:

2 Likes

Yes that’s correct! Just had to change how component was defined in the import but the rest ended up working so that made me happy as this opens up all the other things we can draw now

1 Like

I found a bug related to this and will fix for next 8.6 RC

1 Like

Thanks @eirannejad !

EDIT:

Just updated to version:
8.6.24079

From version:
8.6.24074

And now the working code I shared above is giving an odd error about items can’t be null.

Error:

  1. Error running script (ArgumentNullException): Value cannot be null. (Parameter ‘item’)

Full Code:

import System
import Rhino
import Grasshopper as gh
import ghpythonlib.component as component  # Changed to this for Python 3

__author__ = "Michael Vollrath"
__version__ = "2024.03.20"
# Made With <3 In Dallas, TX
# Modification of script originally created by Seghier Khaled

ghenv.Component.Name = "Curve Highlight"
ghenv.Component.NickName = "CH"
ghenv.Component.Description = "Custom Preview component allowing thickness and color preview of curves drawn in the foreground"

ghenv.Component.Params.Input[0].Name = "Curve"
ghenv.Component.Params.Input[0].NickName = "C"
ghenv.Component.Params.Input[0].Description = "Input Curves"

ghenv.Component.Params.Input[1].Name = "T"
ghenv.Component.Params.Input[1].NickName = "T"
ghenv.Component.Params.Input[1].Description = "Curve Thickness"

ghenv.Component.Params.Input[2].Name = "Color"
ghenv.Component.Params.Input[2].NickName = "Pc"
ghenv.Component.Params.Input[2].Description = "Preview Color"


class MyComponent(component):

    def RunScript(self,
            C: System.Collections.Generic.IList[Rhino.Geometry.Curve],
            T: System.Collections.Generic.IList[int],
            Pc: System.Collections.Generic.IList[System.Drawing.Color]):

        msg_level = ghenv.Component.RuntimeMessageLevel.Warning

        # Add warning message
        if not C or len(C) == 0:
            ghenv.Component.AddRuntimeMessage(msg_level, "Input C Failed To Collect Data")
            non_null_curves = []
        else:
            # Filter out null curves
            non_null_curves = [curve for curve in C if curve is not None]

        # Set default value for thickness if T is None or empty
        if not T or len(T) == 0:
            T = [10.0]

        # Set default value for color if Pc is None or invalid
        if not Pc or any(color is None for color in Pc):
            Pc = [System.Drawing.Color.OrangeRed]

        # Ensure all input lists have the same length
        max_length = max(len(non_null_curves), len(T), len(Pc))
        # Repeat the elements in the list to match the max_length
        self.cols = [Pc[i % len(Pc)] for i in range(max_length)]
        self.thickness = [T[i % len(T)] for i in range(max_length)]
        self.black = System.Drawing.Color.Black
        self.curves = non_null_curves

    def DrawForeground(self, sender, arg):

        if (not ghenv.Component.Hidden and not ghenv.Component.Locked
            and ghenv.Component.OnPingDocument() == gh.Instances.ActiveCanvas.Document
            and arg.Viewport.Id == arg.RhinoDoc.ActiveDoc.Views.ActiveView.ActiveViewportID):

            # Draw Stuff To Foreground
            for i in range(len(self.curves)):
                # Draw input Curve(s)
                arg.Display.DrawCurve(self.curves[i], self.cols[i], self.thickness[i])

    def __enter__(self):
        Rhino.Display.DisplayPipeline.DrawForeground += self.DrawForeground

    def __exit__(self):
        Rhino.Display.DisplayPipeline.DrawForeground -= self.DrawForeground

Would you mind sending me a file that replicates the error? Here is what I am seeing.

Hi @eirannejad,

Certainly, please see the attached .gh file containing the problem repeated.

Below red circles represent unset/null inputs, green are valid inputs.

If we have a valid curve and color, it works:

If we have a valid curve and no color connected, it works:

However, if we do not have a curve and the Pc input is connected, it throws the error:

20240321_Script_Null_Input_Issue_01a.gh (14.9 KB)

1 Like

@michaelvollrath I just installed and tested this in 8.6.24074.01001 and it throws the same error so it doesn’t seem to be a regression.

I will debug and get it fixed. Thanks for reporting it :pray:

1 Like