Creating Rhino plug-ins

written by Nathan 'jesterKing' Letwory

This book is an introduction into writing a Rhino plug-in. For this plug-in we are going to use Visual Studio Code and dotnet tools. Throughout the book we will touch upon as many areas of Rhino plug-in development as we can fit: commands, visualization, custom objects, user data, localization and so on.

This book is made largely during the live coding stream sessions on the playlist Live Coding Rhino. Some work is done off-screen, mainly improving on the text and structure of the book and sometimes smaller fixes to code. In most cases such code fixes are later on handled in the live streams.

The project is implemented using the literate programming paradigm utilizing the literate programming extension for Visual Studio Code. After reading this book the reader will have also read all the source code for the complete project.

Setting up the project

Rhino plug-ins are essentially a DLL with the extension renamed to RHP. To that end we create a .csproj file that defines the project and its properties contains the project description.

<<jesterLiveCode project file.*>>=
<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<<jesterLiveCode project properties>>
	</PropertyGroup>
	<ItemGroup>
		<<jesterLiveCode project package references>>
	</ItemGroup>
	<ItemGroup>
		<<jesterLiveCode source files>>
	</ItemGroup>
</Project>

By default a dotnet project will be compiled against a .NET core framework, but Rhino 7 currently uses .NET Framework 4.8. To have the project compile as a Rhino plug-in, we need to target the .NET 4.8 framework. A future version of Rhino will be using more modern frameworks, but for now we will stick with what Rhino uses.

<<jesterLiveCode project properties>>=
<TargetFramework>net48</TargetFramework>

We need to tell the project that we want an RHP file, not a DLL. For this we are going to use the <TargetExt> tag.

<<jesterLiveCode project properties>>=+
<TargetExt>.rhp</TargetExt>

We also want to control the project management manually. This means we need to disable default items, as well as disable automatic assembly information generation. This can be done with the tags <EnableDefaultItems> and <GenerateAssemblyInfo>. With these settings in place we can now control what files get compiled and where the auxiliary plug-in information is held. All files we care about will be added to <<jesterLiveCode source files>>.

<<jesterLiveCode project properties>>=+
<EnableDefaultItems>False</EnableDefaultItems>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>

To be able to use RhinoCommon SDK we need to add the RhinoCommon package reference from NuGet. The current version we are going to use is 7.12.21313.6341. This means that the minimum required version of Rhino for this plug-in is Rhino 7.12.

<<jesterLiveCode project package references>>=
<PackageReference Include="RhinoCommon" Version="7.12.21313.6341"/>

To be able to debug using the Windows PDB format, this needs to be configured in the project settings. We want to use the full format for PDB files, not the portable format. Furthermore we want to enable debug symbols at all times.

<<jesterLiveCode project properties>>=+
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>

Creating the plug-in class

A Rhino plug-in derives from one of the plug-in classes that is provided by the Rhino.PlugIns namespace. There are several available, for import, export, and so. But in our case we are going to do just a simple utility plug-in. Such a plug-in derives from Rhino.PlugIns.PlugIn, the base class for all plug-in types.

Rhino initializes plug-ins slightly differently depending on what class is used to derive from. For instance a Rhino.PlugIns.RenderPlugin implementation in an RHP will cause Rhino to automatically look for and register implementations of RenderContent types.

<<jesterLiveCode plug-in.*>>=
using Rhino;
using Rhino.PlugIns;

namespace jesterLiveCode {

    public class jesterLiveCodePlugin : PlugIn
    {
        <<jesterLiveCode plug-in construction>>
    }
}

The source file needs to be added to the project to have it compiled, since we are managing the project manually.

<<jesterLiveCode source files>>=
<Compile Include="jesterLiveCodePlugin.cs" />

Rhino plug-ins are going to be initialized by Rhino, and for plug-ins there should always be only one instance. That means a singleton pattern here is going to be needed.

<<jesterLiveCode plug-in construction>>=
public jesterLiveCodePlugin()
{
    if(Instance == null)
    {
        Instance = this;
    }
}

public static jesterLiveCodePlugin Instance { get; private set; }

Setting up the assembly information

For Rhino to be able to load the plug-in properly the correct assembly attributes need to be present. The PlugInDescription attribute needs to be set with several different types. They need to be present, but information can be filled out as available or deemed necessary to divulge. Also the assembly needs to have a unique GUID. Finally there needs to be file versioning.

<<jesterLiveCode assembly info.*>>=
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Rhino.PlugIns;

<<jesterLiveCode plugin description>>
<<jesterLiveCode assembly information>>
<<jesterLiveCode plugin guid>>
<<jesterLiveCode plugin versioning>>

Add the assembly information source file to the project.

<<jesterLiveCode source files>>=+
<Compile Include="Properties/AssemblyInfo.cs" />

Plug-in description and contact information

The plug-in description attributes are used to show the given information in the Rhino plug-in manager. Additionally in some cases Rhino may show users a dialog with the contact information from the plug-in description attributes, most notably when plug-in initialization fails. That is provided as a mechanism to users to report problems and find out about workarounds and fixes.

At minimum an email address and an update URL would be good to provide. Any other information that give users enough information for contact and who is responsible for the pluge-in would also be useful.

<<jesterLiveCode plugin description>>=
[assembly: PlugInDescription(DescriptionType.Address, "Turku")]
[assembly: PlugInDescription(DescriptionType.Country, "Finland")]
[assembly: PlugInDescription(DescriptionType.Email, "jesterking@letwory.net")]
[assembly: PlugInDescription(DescriptionType.Phone, "-")]
[assembly: PlugInDescription(DescriptionType.Fax, "-")]
[assembly: PlugInDescription(DescriptionType.Organization, "Letwory Interactive Oy")]
[assembly: PlugInDescription(DescriptionType.UpdateUrl, "https://github.com/jesterKing/rhics")]
[assembly: PlugInDescription(DescriptionType.WebSite, "https://www.letworyinteractive.com")]

Assembly information

The assembly information attributes are visible in Windows through the file explorer. File details will allow users to investigate the RHP file before loading it into Rhino should they have acquired an archive outside of the package manager mechanism.

General information

<<jesterLiveCode assembly information>>=
[assembly: AssemblyTitle("jesterLiveCode")] // Plug-In title is extracted from this
[assembly: AssemblyDescription("A plug-in showing the ropes of writing code for Rhino.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Letwory Interactive Oy")]
[assembly: AssemblyProduct("jesterLiveCode")]
[assembly: AssemblyCopyright("Copyright ©  2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

Plug-in ID

As part of the assembly information a GUID is specified as well. Here great care should be taken to ensure it is actually unique and not one copied from sample code, like the one from this book. Conflicting GUIDs will cause Rhino to complain about duplicate IDs and further ignore the plug-in during attempts to load it. In other words the assembly GUID is also the Rhino plug-in ID.

<<jesterLiveCode plugin guid>>=
[assembly: Guid("0fda72f0-ccd3-4b75-8896-f495ae82fc18")]

Plug-in versioning

It is best to follow the versioning schema as used by Microsoft using four digits seperated by dots. This can be a combination of major version, minor version, build number, revision. The most important aspect here is to keep consistent so that versions and releases can be tracked properly, especially with regard to bug reports, but also for roadmapping future development.

<<jesterLiveCode plugin versioning>>=
[assembly: AssemblyVersion("0.0.1.0")]
[assembly: AssemblyFileVersion("0.0.1.0")]

Creating a command

A command in Rhino derives from Rhino.Commands.Command. There two mandatory parts for a command to implement to get a minimal implementation.

For this command we are going to start a fragment where we are going to collect all the namespace usages.

We need at least the Rhino.Commands namespace. And lets also bring in Rhino, since that is where RhinoDoc lives.

<<first command usings>>=
using Rhino;
using Rhino.Commands;

Our command class is going to be called jesterBoxCommand, which will generate a custom object (TBD) that can be eventually controlled via a custom panel. While our command is active we'll be drawing temporary results with the dislay conduit called jesterBoxConduit.

Apart from the necessary command overrides there will be some utilities implementated in this class that can be used for creating and visualizing our object. These will be provided in <<jesterBox command utilities>>.

<<jesterLiveCode source files>>=+
<Compile Include="jesterBoxCommand.cs" />
<<first command class.*>>=
<<first command usings>>

namespace jesterLiveCode
{
    public class jesterBoxCommand: Command
    {
        <<first command overrides>>
        <<jesterBox command utilities>>
    }
}

The mandatory overrides for a Command class

English name of the command

To get a minimal working Command the property EnglishName. This property will essentially be the name of the command as you type it into the Rhino command-line.

<<first command overrides>>=
public override string EnglishName => "jesterBox";

Local name of the command

We are localizing our plug-in. To that end we need to override the LocalName property. Note that we do need access to the Localization class of the Rhino.UI namespace to get access to the LocalizeString method. LocalizeString is going to be the main work horse for our localization efforts.

We need the assembly object of our plug-in to pass on to the localization system. The System.Reflection namespace will give us the tools we need for that.

<<first command overrides>>=+
public override string LocalName {
    get {
        Assembly ass = Assembly.GetExecutingAssembly();
        return Localization.LocalizeString("jesterBox", ass, 1);
    }
}
<<first command usings>>=+
using System.Reflection;
<<first command usings>>=+
using Rhino.UI;

We also need to override the method RunCommand, which is going to be the actual meat of the command. Here the driving logic of the command will live. We want to add an object, a box actually, with given dimensions to a place where the user has picked a point.

RunCommand implementation

The center of our command is the implementation of RunCommand. This method gets run when the command is used on the Rhino command-line, through buttons on toolbars or entries in menus, and through scripting.

Upon execution the command is given a reference to the document for which it is being run as well as the type of interaction with the command. This is a way to distinguish between command-line usage and potential GUI usage. It is customary for commands to implement the two workflows where it makes sense. Command-line usage means no dialogs are going to be popped up to request input from the user. This is useful especially for usage in scripts and automation, but users may prefer using command-line based interaction only. For the interactive workflow commands have the opportunity to use a custom GUI to allow the user to interact with the command. Data could be presented in perhaps a more clear layout. Such GUI generally is modal and needs to be closed either through acceptance or cancellation.

For jesterBox we are going to implement only command-line interaction.

<<first command overrides>>=+
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
    <<add command options>>
    <<set up jesterBox display conduit>>
    <<get point>>
    <<handle get loop>>
    <<add the object>>
    return Result.Success;
}
Command options

First we need to add the command options. In our case we are going to use just simple integer options for the dimensions of the box. To be able to use the custom input classes we need to ensure we have the proper namespace added. The Rhino Get classes return also a GetResult, which is provided by the Rhino.Input namespace.

<<first command usings>>=+
using Rhino.Input;
using Rhino.Input.Custom;

Now we are able to use all classes provided by the namespace Rhino.Input.Custom.

<<add command options>>=
Point3d point = Point3d.Unset;
int width = 10;
int length = 10;
int height = 10;

OptionInteger widthOption = new OptionInteger(width, 1, 50);
OptionInteger lengthOption = new OptionInteger(length, 1, 50);
OptionInteger heightOption = new OptionInteger(height, 1, 50);
GetPoint instance

As mention we want to add the box to a user picked position. For that we need the GetPoint class. For our project we implement a custom GetPoint class, jesterBoxGetPoint, which we will be using to handle interactive mouse movements and display some intermediate results while our command is still active. And we can add the options to the instance of that class when we have one. It is also good to tell the user what is expected from them, so lets set the command-prompt to "Select a location". The user is supposed to press Enter when they are happy with the given settings. To this end we need to ensure that our jesterBoxGetPoint instance actually can accept nothing.

<<get point>>=
jesterBoxGetPoint getPoint = new jesterBoxGetPoint(conduit);
point = conduit.Location = conduit.PotentialLocation;
getPoint.AcceptNothing(true);
getPoint.SetCommandPrompt("Pick a location, and enter when done");
getPoint.AddOptionInteger("Width", ref widthOption);
getPoint.AddOptionInteger("Length", ref lengthOption);
getPoint.AddOptionInteger("Height", ref heightOption);

Since we are getting a Point3d instance we need to ensure that we have the Rhino.Geometry namespace available to us.

<<first command usings>>=+
using Rhino.Geometry;
using System.Collections.Generic;
Initialize the display conduit

To visualize the user input we need our jesterBoxConduit. We need to give it all the information it needs to draw properly. First however we need to initialize an instance that we can use.

<<set up jesterBox display conduit>>=
jesterBoxConduit conduit = new jesterBoxConduit();

conduit.Location = point;
conduit.Width = width;
conduit.Height = height;
conduit.Length = length;

conduit.Enabled = true;
Input loop handling

Now we can start handling the get loop. The loop will be essentially an eternal loop with proper exit conditions sprinkled throughout the loop.

Each time an option is changed, or a point is clicked we need to update the conduit as well to ensure proper visualization of the current settings.

<<handle get loop>>=
for(;;)
{
    GetResult result = getPoint.Get();

    if(result == GetResult.Point)
    {
        point = getPoint.Point();
        conduit.Location = point;
        RhinoApp.WriteLine($"User clicked point {point}");
        continue;
    }

    if(result == GetResult.Option)
    {
        width = widthOption.CurrentValue;
        length = lengthOption.CurrentValue;
        height = heightOption.CurrentValue;

        conduit.Width = width;
        conduit.Height = height;
        conduit.Length = length;

        RhinoApp.WriteLine($"Current given dimensions {width}x{length}x{height}");
        continue;
    }

    if(result == GetResult.Nothing)
    {
        break;
    }

    if(result == GetResult.Cancel)
    {
        conduit.Enabled = false;
        RhinoApp.WriteLine("Command cancelled");
        return Result.Cancel;
    }
}
Adding the object

Once we get out of the loop and are still in the command we can add the object to the specifications given by the user on the command-line. If no point was specified by the user when we get to this point we are going to use the PotentialLocation as known by the conduit.

To add the object we use the CreateJesterBox method provided by <<jesterBox command utilities>>.

We also disable our conduit, so it doesn't get called all the time for nothing.

<<add the object>>=
conduit.Enabled = false;
if(!point.IsValid) {
    point = conduit.PotentialLocation;
}
Brep boxBrep = CreateJesterBox(point, width, height, length);
doc.Objects.AddBrep(boxBrep);
doc.Views.Redraw();
<<jesterBox command utilities>>=
static public Brep CreateJesterBox(Point3d point, int width, int height, int length)
{
    Plane p = new Plane(point, Vector3d.ZAxis);
    Interval widthInterval = new Interval(0, width);
    Interval lengthInterval = new Interval(0, length);
    Interval heightInterval = new Interval(0, height);
    Box box = new Box(p, widthInterval, lengthInterval, heightInterval);
    Brep boxBrep = box.ToBrep();

    if(boxBrep == null)
    {
        System.Diagnostics.Debug.WriteLine("Our brep is not good :/");
        System.Diagnostics.Debug.WriteLine($"{point}, {width}x{height}x{length}");
        System.Diagnostics.Debug.WriteLine($"...");
    }

    return boxBrep;
}

Creating a DisplayConduit

To be able to show intermediate results of our command while it is still running we can use a DisplayConduit. This will allow us to draw results without having to actually add geometry to the document.

To implement the DisplayConduit we need to inherit Rhino.Display.DisplayConduit, and at least implement calculation of scene bounding box and a draw method. For our command and display conduit we are going to draw in DrawOverlay. We also want to be able to pass on the data to be visualized, which we'll do with <<jesterBoxConduit data access>>.

<<jesterLiveCode source files>>=+
<Compile Include="jesterBoxConduit.cs" />
<<jesterBox display conduit.*>>=
<<jesterBox usings>>

namespace jesterLiveCode {
    public class jesterBoxConduit : DisplayConduit
    {
        <<jesterBoxConduit data access>>
        <<jesterBoxConduit calculate bounding box>>
        <<jesterBoxConduit draw overlay>>
    }
}

We must not forget to add the proper namespace so we can use it in our class implementation:

<<jesterBox usings>>=
using Rhino.Display;

Data access

The display conduit implementation will provide several properties to set and read the information we want to visualize. This will be for the location, width, height and length of our object.

<<jesterBoxConduit data access>>=
public Point3d Location { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Length { get; set; }

Not only do we need the dimensions and location of the object to be added, we also want to know the potential location of the object under the mouse cursor.

<<jesterBoxConduit data access>>=+
public Point3d PotentialLocation { get; set; }

Since we need Point3d we also need to bring in the Rhino.Geometry namespace.

<<jesterBox usings>>=+
using Rhino.Geometry;

Calculating the bounding box

To be able to properly draw our geometry we need to tell the display pipeline what the bounding box is for our geometry. Without the proper bounding box geometry will easily get clipped.

<<jesterBoxConduit calculate bounding box>>=
protected override void CalculateBoundingBox(CalculateBoundingBoxEventArgs bbe)
{
    base.CalculateBoundingBox(bbe);
    Brep box = jesterBoxCommand.CreateJesterBox(Location, Width, Height, Length);
    Brep previewBox = jesterBoxCommand.CreateJesterBox(PotentialLocation, Width, Height, Length);
    BoundingBox bb = box.GetBoundingBox(false);
    BoundingBox previewBb = previewBox.GetBoundingBox(false);

    bbe.IncludeBoundingBox(bb);
    bbe.IncludeBoundingBox(previewBb);
}

Drawing the temporary geometry

The display conduit will be drawing not just one preview, but also a second preview based on the mouse cursor movements. The first, and main, preview of the object will be drawn with the dimensions entered on the command-line at the location specified by user clicks in the world.

However, when the user moves the mouse cursor a secondary preview will be shown at the world location determined by the mouse location. The secondary preview uses a different color for the drawing.

<<jesterBoxConduit draw overlay>>=
protected override void DrawOverlay(DrawEventArgs drawe)
{
    base.DrawOverlay(drawe);
    if(Location.IsValid)
    {
        Brep box = jesterBoxCommand.CreateJesterBox(Location, Width, Height, Length);
        drawe.Display.DrawBrepWires(box, System.Drawing.Color.Blue);
    }
    Brep previewBox = jesterBoxCommand.CreateJesterBox(PotentialLocation, Width, Height, Length);
    drawe.Display.DrawBrepWires(previewBox, System.Drawing.Color.Gray);
}

Implementing the custom GetPoint class jesterBoxGetPoint

The custom GetPoint class will be used to communicate mouse location as Point3d data to the display conduit for drawing a preview of the preview. We want to show where the new location would end up.

A jesterBoxConduit instance is passed to our GetPoint implementation when it is constructed. Our class needs to hold on to it, so during OnMouseMove the conduit can be updated.

<<jesterLiveCode source files>>=+
<Compile Include="jesterBoxGetPoint.cs" />
<<jesterLiveCode jesterBoxGetPoint.*>>=
<<jesterBoxGetPoint imports>>

namespace jesterLiveCode {
    public class jesterBoxGetPoint : GetPoint
    {
        jesterBoxConduit _conduit;

        public jesterBoxGetPoint(jesterBoxConduit conduit) {
            _conduit = conduit;
        }

        <<jesterBoxGetPoint public API>>
    }
}

Since we are inheriting GetPoint we need the Rhino.Input.Custom namespace. We need also access to Point3d from the Rhino.Geometry namespace.

<<jesterBoxGetPoint imports>>=
using Rhino.Input.Custom;
using Rhino.Geometry;

There are two parts that work together through our GetPoint implementation: overriding the OnMouseMove method and providing a property PotentialLocation on the conduit. The latter we introduced in <<jesterBoxConduit data access>>.

Our OnMouseMove implementation is only going to harvest the Point property from the event argument passed to it when called. This we save to the display conduit for further usage.

<<jesterBoxGetPoint public API>>=
protected override void OnMouseMove(GetPointMouseEventArgs e)
{
    _conduit.PotentialLocation = e.Point;
}

This mechanism ensures we have always the current world location based on the mouse cursor movements available in the display conduit. With this information we can draw a secondary preview of the object to be added by the command.

Localizing the command

Earlier we used Localization.LocalizeString to set up our code for localized versions of the command jesterBox. We need now the localization files that are formatted with XML. The files should be located next to the RHP file our project generates.

For this project lets try to localize some strings to Finnish and Dutch. For now we are going to assume that correct language identifier has been set with Python in a separate script. For Finnish the LCID is 1035 and for Dutch it is 1043.

Finnish localization

<<jesterLiveCode source files>>=+
<EmbeddedResource Include="fi-fijesterLiveCode.xml">
    <Link>Localization\fi-fijesterLiveCode.xml</Link>
</EmbeddedResource>
<<jesterBox finnish localization.*>>=
<?xml version="1.0" encoding="utf-8"?>
<RMA_LOCALIZATION>
  <RMASTRING type="string" English="jesterBox[[1]]" Localized="jesterLaatikko[[1]]" file_name="jesterBoxCommand.cs" />
</RMA_LOCALIZATION>