Rhino Beta Feature: In-Viewport User Interface

Rhino 9 adds an SDK for in-viewport user interface objects - widgets that draw inside a viewport and respond to the mouse. Instead of pushing users out to a command line or panel, you can put the control right next to what it controls.

We have been using this internally for a while now and feel it is worth mentioning. If you have run the Rhino 9 BETA you may have already used it:

  • ArrayCrvAdvanced - the on-curve span grips, the size and spacing arrows, the rotation/pitch/roll rings.
  • Patch - the continuity badges on each constraint curve; click to step through G0/G1/G2
  • GlobalEdgeContinuity - the numbered, color-coded dot at each evaluated edge; click one to select and zoom to that edge
  • ScaleEach / RotateEach - the in-viewport sliders you drag to set the start and end factor
  • SubDMatch - the bulge direction arrows
  • Markup - the whole floating tool HUD
  • Grasshopper's Curve Widget and Angular Widget components
  • EditHatchGradient - drag the gradient anchors and color stops right on the hatch
  • SetHatchBaseAndRotation - the on-screen protractor

Two kinds of UI objects

World-space objects live at a 3d point in the model and pan and zoom with the scene. The grip classes can additionally be dragged in 3d, constrained to a curve, and osnapped.

Screen-space controls are 2d widgets aligned to the viewport. They can optionally “stick” to a 3d point so they follow an object around while staying screen-aligned and screen-sized.

Hello UI Widget

Here’s a basic sample that adds a grip widget to the document that users can drag around.

using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.UI;

class MyGrip : GripUserInterfaceObject
{
  public MyGrip(Point3d location) : base(location)
  {
    GripFillColor = System.Drawing.Color.Orange;
  }

  protected override void OnDrag(Point3d newLocation, MouseState mouse)
  {
    base.OnDrag(newLocation, mouse);
    RhinoApp.WriteLine($"grip is at {GripLocation}");
  }
}

public class SampleGripCommand : Command
{
  public override string EnglishName => "SampleGrip";
  protected override Result RunCommand(RhinoDoc doc, RunMode mode)
  {
    // If the grip has already been added, remove it. Otherwise, add one
    // The id is used to define a group of user interface objects.
    // Using the command's Id is a good convention
    bool gripExisted = doc.ViewUserInterface.RemoveByGroupId(this.Id) > 0;
    if (!gripExisted)
    {
      doc.ViewUserInterface.Add(new MyGrip(new Point3d(0, 0, 0)), this.Id);
    }
    // Note that the command returns immediately for this sample and the grip stays "live"
    doc.Views.Redraw();
    return Result.Success;
  }
}

Screen controls

var btn = new UserInterfaceButton
{
  Text = "Bake",
  Location = new System.Drawing.Point(10, 100),
  HorizontalAlignment = ControlHorizontalAlignment.Left,
  VerticalAlignment = ControlVerticalAlignment.Bottom
};

// Instead of text, you can also set an SVG string to the button
btn.Click += (s, e) => RhinoApp.WriteLine("clicked");
doc.ViewUserInterface.Add(btn, Id);

var slider = new UserInterfaceSlider
{
  Location = new System.Drawing.Point(10, 140),
  HorizontalAlignment = ControlHorizontalAlignment.Left,
  VerticalAlignment = ControlVerticalAlignment.Bottom,
  Range = new Interval(0, 10),
  Value = 5,
  DigitPrecision = 0        // 0 == integers only
};
slider.ValueChanged += (s, e) => RhinoApp.WriteLine($"{slider.Value}");

doc.ViewUserInterface.Add(slider, Id);

Modifying the document from a mouse handler

You are inside a mouse callback, not a command, so undo records and getters are not set up the way you would expect. In C++ there is an explicit path for this — call RunCommand(mouse) and do your work in the `OnRunCommand` override, which runs in proper command scope.


C++

The same API is available in the C++ SDK, in rhinoSdkUserInterfaceObject.h

RhinoCommon C++
UserInterfaceObjectBase CRhinoUserInterfaceObject
GripUserInterfaceObject CRhinoGripUserInterfaceObject
DirectionGripUserInterfaceObject CRhinoDirectionGripUserInterfaceObject
RotationGripUserInterfaceObject CRhinoRotationGripUserInterfaceObject
TextDotUserInterfaceObject CRhinoTextDotUserInterfaceObject
UserInterfaceControl CRhinoUserInterfaceControl
UserInterfaceButton / Slider / Icon CRhinoUserInterfaceButton / Slider / Icon
doc.ViewUserInterface.Add(...) CRhinoDoc::AddUserInterfaceObject(...)
RemoveByGroupId(...) CRhinoDoc::RemoveUserInterfaceObjects(groupId)

More to come

Building out an entire user interface library is a massive undertaking. We’ll continue to add controls, but it needs to have priority. Please let us know what you need so we can prioritize.

Very nice addition and a good move towards a more modern and efficient interface.
The most needed thing is Coherency. Make UI rules before going too deep into the development.