Site Tools

Essentials of DotNET Plug-in Programming

So, you are new to DotNET?

So, you're probably pretty excited and scared right now? You have no idea where to start? You're likely to make some major mistakes in the first few hours which will prove to be frustrating? You'd like some basic advice? Let's get started…

People who start writing DotNET plug-ins for Rhino basically fall into three groups:

  • C++ programmers who want to be hep
  • RhinoScript programmers who want to do more
  • DotNET programmers who are sick and tired of writing everything from scratch

Unfortunately, none of these groups has a smooth transitional period. It's difficult for C++ programmers because the DotNET paradigm differs in key areas from unmanaged code (no pointers, no references, no memory management). It's difficult for RhinoScripters because they suddenly have to deal with object-oriented programming (OOP). And it's difficult for dyed-in-the-wool DotNETters because the Rhino SDK doesn't work much like the DotNET platform namespaces and classes.

If you're a scripter, you have a lot to learn about classes, types, instances, and encapsulation (among a lot else), and this isn't the place for that. The web is frothing with generic DotNET developer forums and repositories which will get you a long way down the road. If you're already comfortable with object-oriented programming, this page will teach you some key Rhino SDK concepts that will eliminate some frustration.

This wiki page explains some of the key concepts of Rhino plug-in development which are not immediately obvious to newcomers.

Const and non-const instances

Every class in the Rhino SDK comes in two flavors, a const and a non-const one.

Const instances all start with “I” (such as IOnPlane, IOnBoundingBox, IRhinoCurveObject and IRhinoEventWatcher). You cannot change these instances, and they lack all the functions that alter the memory state. For example, IOn3dPoint lets you retrieve x, y and z coordinates, but not set them.

Non-const objects start with “On” if they are part of openNURBS. They start with “MRhino” if they are part of Rhino (such as OnPlane, OnBoundingBox, MRhinoCurveObject and MRhinoEventWatcher). These instances can be altered without problems.

If you need to alter a plane, but the instance you've got is of type IOnPlane, you need to make a copy of the const instance. All the types in the SDK have copy constructors, which take another instance of the same type and make an exact (but always non-const) duplicate:

Dim myPlane As New OnPlane(someoneelsesPlane)
OnPlane myPlane = new OnPlane(someoneelsesPlane);

SomeoneelsesPlane is either an IOnPlane (const) or OnPlane (non-const). Sometimes a class will expose some duplication functions, which are to be preferred over copy-constructors:

Dim myCurve As OnCurve = someoneelsesCurve.DuplicateCurve()
OnCurve myCurve = someoneelsesCurve.DuplicateCurve();

Since OnCurve is an abstract class, it doesn't have any constructors to begin with, which is why you don't have an option in this case.

Gotcha for C++ developers
You've probably caught on that having two versions for each class is a way to mimic the 'const' keyword in C++. Rhino is written in C++ and uses constness all over the place. However, the DotNET framework lacks a keyword that operates like const. So we had to use this horrific hack to let DotNET plug-ins to use Rhino's unmanaged C++ kernel. So, On3dPoint in DotNET is the same as ON_3dPoint in C++. IOn3dPoint is the same as const ON_3dPoint.

SDK class hierarchy

The class hierarchy in openNURBS and Rhino is straightforward, although a bit counter-intuitive at times. For example, any curve object in Rhino is an instance of MRhinoCurveObject. (Object is something that can be selected with the mouse.) MRhinoCurveObject itself has a member of the abstract type OnCurve, meaning that it can contain all the different curve types that inherit from this abstract class (such as OnNurbsCurve, OnArcCurve, OnLineCurve, and OnPolyCurve). But, you'll find types in openNURBS which are clearly mathematical curves, and they do not derive from OnCurve. Examples are OnCircle, OnArc, OnLine, and OnBezierCurve. The thinking is that these classes are so atomic, that they should be as light-weight as possible. That is why there is both an OnArc and an OnArcCurve class.

Atomic type Curve type
OnLine OnLineCurve
OnPolyline OnPolylineCurve
OnCircle OnArcCurve
OnArc OnArcCurve
OnEllipse No special curve type, you have to convert ellipses to OnNurbsCurve
OnBezierCurve No special curve type, you have to convert beziers to OnNurbsCurve
OnPolynomialCurve Forget it, you don't want to use this class

The same logic applies to surfaces. There is an abstract class called OnSurface, but not all surface types derive from it. OnSphere for example does not. That is why you need to convert an OnSphere into some other kind of surface before you can add it to the document (because MRhinoSurfaceObject only accepts instances that derive from OnSurface).

Atomic type Surface type
OnPlane OnPlaneSurface
OnSphere OnNurbsSurface or OnRevSurface
OnCylinder OnNurbsSurface or OnRevSurface or OnBrep if you also want the caps
OnCone OnNurbsSurface or OnRevSurface or OnBrep if you also want the cap
OnBezierSurface No special surface type, you have to convert beziers into OnNurbsSurface
OnPolynomialSurface Seriously, stay away…

The SDK helpfile lists the inheritance tree of each type (including all the interfaces it implements). Some functions (such as MRhinoDoc.AddCurveObject()) have overloads for different types of curve classes. Because these functions accept many types, don't worry about converting one type into another. Whenever you encounter a function with overloads, always look if there's one that saves you work.

Rhino Document layout

Everything inside the Document is fixed. You are not allowed to change anything. This may sound odd, since you're changing things all the time as a regular user. The thing to realize is that 'changing' is not the same as 'replacing'. Once a curve makes it into the Rhino document, the only way to change it is to replace it with another curve that looks similar. The reason for this bottlenecking is that we need to keep track of all the changes made, so we can keep the undo records in synch. If plug-ins changed the object colors without telling anybody, there'd be no way to maintain a correct undo-stack.

If you want to transform an object, or change its layer, or trim the end of a curve, or…, you have to get the const object, make a duplicate, change the duplicate, then switch it out with the old object. For example:

1. Dim obj_ref As New MRhinoObjRef(some_object_id)
2. Dim static_curve As IOnCurve = obj_ref.Curve()
3. Dim mutable_curve As OnCurve = static_curve.DuplicateCurve()
5. mutable_curve.Rotate(0.5 * Math.PI, New On3dVector(0, 1, 0), New On3dPoint(0, 0, 0))
6. RhUtil.RhinoApp().ActiveDoc().ReplaceObject(obj_ref, mutable_curve)
1. MRhinoObjRef obj_ref = New MRhinoObjRef(some_object_id);
2. IOnCurve static_curve = obj_ref.Curve();
3. OnCurve  mutable_curve = static_curve.DuplicateCurve();
5. mutable_curve.Rotate(0.5 * Math.PI, New On3dVector(0, 1, 0), New On3dPoint(0, 0, 0));
6. RhUtil.RhinoApp().ActiveDoc().ReplaceObject(obj_ref, mutable_curve);

1. Create a new object reference based on an ID (we're assuming the ID is a valid
   one and points to a curve object)
2. Get a pointer to the curve geometry in the document. Since the curve comes from
   the document, it will be const.
3. Duplicate the curve, so we get a non-const version of the same geometry. Changes
   we make to mutable_curve will not affect static_curve in any way.
5. Rotate the non-const curve through 90 degrees
6. Use the ReplaceObject() function to switch out the old geometry with the new
   (while maintaining the same attributes; layer, name, colour etc.)

This particular example is a bit redundant, since the Document already has a function called TransformObject() which does all this duplicating and replacing behind the scenes.

Command-line UI and what it means for you

Rhino is primarily a command-line based application. We have menus and toolbars, but these UI elements merely send a text command into the command-line, which then parses the text and calls the appropriate code. Because we know that 99% of all Rhino functionality is executed inside commands, we make assumptions about optimization and framework design which make it easier for people to write plug-ins, provided they agree to play nice.

For example, if you stick with commands, you never have to worry about undo-records, scriptability, and error trapping since Rhino takes care of all this. As soon as you start doing stuff out of the blue, you are responsible for making sure everything keeps on running smoothly. A prime example of non-command features are the ones initiated from a non modal dialog. Non modal dialogs are usually displayed by calling a certain command, but because they are non modal, the command immediately completes while leaving the dialog on the screen. There is no reason why you couldn't use the code from within a button on a non modal dialog, but in practice there is a huge difference:

  • Any exceptions that you fail to catch in time will crash Rhino. Commands themselves are smart enough not to allow crashes to propagate too far.
  • Any objects you add/delete/modify in your code are not included in the undo records. You have to start and end undo records if you want to do stuff outside of commands.
  • Your functionality cannot be scripted by users, since they have no mechanism of automating pressing buttons on your dialogs. If you have a command line interface to your functionality, people can write both macros and scripts that use your code more effectively.
  • There is no failsafe against multiple things running at the same time. Some commands can be nested, but typically you are not allowed to do naughty stuff while a command is running. If a user decides to delete an object that you are using while you are using it, you will most likely crash out.

If you do want to use non modal dialogs, the best way to solve these issues is to make commands for all the features in your plug-in, and have your dialog buttons call those commands instead of running the code themselves.

developer/dotnetpluginessentials.txt · Last modified: 2020/08/14 (external edit)