Grasshopper2 Plugin Development Support

Hi @DavidRutten

I’ve been exploring Grasshopper2 as a developer. Since documentation doesn’t exist yet, I’m trying my best to understand how things work and how they’re different from GH1. I’m curious about a few specific things:

  1. What is a Pear? :smiley:

  2. When/Why is anything Tentative in the UI?

  3. Do you have any timelines in mind for a “Refactoring Guide” to help port GH1 plugins to GH2?

I have already gone through the following forum/blog posts:

@cp1

Another question;
4. How do I override the Mouse interaction responses (such as RespondToMouseDown) for a component / component’s attributes? Previously this was an overridable function but is no longer so. Is there a different way to do this in GH2?
GH1 (GH_ComponentAttributes) :
public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
GH2 (ComponentAttributes):
Response IResponsiveAttributes.RespondToMouseDown(MouseEventArgs args)

Facing some trouble working with IResponsiveAttributes and responding to certain mouse events. This is the origin of the error I keep bumping into. Am I looking in the wrong place or missing something?

Hi Praneet,

sorry for the late response.

  1. A Pear<T> is a combination of a value and its metadata. It’s actually supposed to be ‘pair’ but I chose pear because that fits better within the nomenclature of data trees. Pears grow on trees. Pairs do not. In addition to value+metadata, pears also allow GH to represent nullness of value types. So actually a pear is a triplet; a value, optional meta data, a boolean indicating whether the value is null.

  2. Tentative UI is the blue junk you see when a wire-drag approaches a component with variable inputs or outputs.
    tentativegrips_edit_0

  3. Nothing solid. I’m still breaking the SDK almost every week. I imagine once GH goes into Beta (still in Alpha at the moment) it will settle enough. I’m hopeful this will happen within one year. Once we go into beta, the focus will be on adding missing components, fixing bugs, and adding missing small stuff like specific undo-records. No more major changes then.

1 Like

Nope, I did not make those methods virtual. They probably should be. I’ll expose them now and upload some code that shows how to use that shortly.

Here’s code for a component which adds a grid underneath it for the input of a complex number:


ComponentWithMouseHandling.cs (6.9 KB)

using System.Numerics;
using Eto.Forms;
using Eto.Drawing;

using GrasshopperIO;
using Grasshopper.UI;
using Grasshopper.UI.Skinning;
using Grasshopper.Components;
using Grasshopper.Extensions;
using Grasshopper.Doc;
using Grasshopper.Doc.Attributes;
using Grasshopper.UI.Flex;
using Grasshopper.UI.Primitives;

namespace Example
{
  [IoId("42f838dd-1895-4b1b-be30-c0b42f838dd2")]
  public sealed class ComponentWithMouseOverrides : Component
  {
    public ComponentWithMouseOverrides()
      : base(new Nomen("Mouse Override", "An example for handling mouse events on custom components.", "Example", "Example"))
    {
      Z = new Complex(0, 0);
    }
    public ComponentWithMouseOverrides(IReader reader) : base(reader)
    {
      Z = reader.Complex128(nameof(Z));
    }
    public override void Store(IWriter writer)
    {
      base.Store(writer);
      writer.Complex128(nameof(Z), Z);
    }
    protected override IAttributes CreateAttributes()
    {
      return new ComponentWithMouseOverridesAttributes(this);
    }

    protected override void AddInputs(InputAdder inputs)
    {
      inputs.AddComplex("Complex Seed", "Cx", "Complex seed for fractal sequence.").Set(Complex.One);
      inputs.AddInteger("Length", "Ln", "Sequence length.").Set(100);
    }
    protected override void AddOutputs(OutputAdder outputs)
    {
      outputs.AddComplex("Sequence", "Sq", "Fractal sequence.", Grasshopper.Parameters.Access.Twig);
    }

    public Complex Z { get; set; }
    protected override void Process(IDataAccess access)
    {
      access.GetItem(0, out Complex seed);
      access.GetItem(1, out int length);

      var sequence = new Complex[length];

      sequence[0] = seed;
      for (int i = 1; i < length; i++)
      {
        seed = seed * seed + Z;
        sequence[i] = seed;
      }
      access.SetTwig(0, sequence);
    }
  }
  public sealed class ComponentWithMouseOverridesAttributes : ComponentAttributes
  {
    public ComponentWithMouseOverridesAttributes(ComponentWithMouseOverrides component) : base(component) { }

    #region layout overrides
    public RectangleF ComplexBounds { get; set; }
    public RectangleF ComplexGrid
    {
      get
      {
        return ComplexBounds.AdjustSides(-4);
      }
    }
    public CircleF ComplexGrip { get; private set; }

    private Grasshopper.UI.Grid.OrthoGrid Grid
    {
      get
      {
        var grid = Grasshopper.UI.Grid.OrthoGrid.DefaultTight;
        grid.Setup(ComplexGrid, ComplexGrid.Center, new SizeF(70, 70));
        return grid;
      }
    }

    protected override void LayoutBounds(Shape shape)
    {
      // This is the last stage in normal component layout.
      // So we're going to let that all happen and only *then*
      // glue our own box to the bottom of the layout.
      base.LayoutBounds(shape);

      // Our added control just adds 150 pixels to the bottom of the component.
      // We can also make it wider, but that involves more work, so I'm not
      // going to tarnish this example. Basically, you can choose to add
      // a constant amount of pixels to the CentralBox by overriding LayoutCentralBox().
      ComplexBounds = RectangleF.FromSides(Bounds.Left, Bounds.Bottom, Bounds.Right, Bounds.Bottom + 150);

      // We must adjust the Bounds now to include our complex box, otherwise Grasshopper thinks
      // the component is a lot smaller than it really is.
      Bounds = RectangleF.Union(Bounds, ComplexBounds);
    }
    #endregion

    #region drawing overrides
    protected override void DrawForeground(Context context, Skin skin, Capsule capsule, Shade shade)
    {
      // Allow the foreground to draw normally.
      base.DrawForeground(context, skin, capsule, shade);

      // We don't want to draw anything outside of the grid,
      // let's set up clipping to ensure that.
      context.Graphics.SetClip(ComplexGrid);

      // Now we'll overlay our Complex plane grid.
      // First fill the grid area with a slightly tinted colour
      // to differentiate it from the component background.
      context.Graphics.FillRectangle(shade.Apex.MoveTo(shade.Edge, 0.1f), ComplexGrid);

      // Then draw the axes and grid lines.
      var grid = Grid;
      grid.Draw(context, Grasshopper.UI.Grid.GridElements.All);

      // Now draw the complex grip.
      var component = (ComponentWithMouseOverrides)Owner;
      grid.GridToControl(component.Z.Real, component.Z.Imaginary, out var x, out var y);

      ComplexGrip = new CircleF(new PointF((float)x, (float)y), 5);
      context.Graphics.FillCircle(OpenColor.Blue9, ComplexGrip);
      context.Graphics.FillCircle(OpenColor.Blue4, ComplexGrip.Offset(-2));

      // Finally draw a strong boundary rectangle around our grid.
      context.Graphics.ResetClip();
      context.Graphics.DrawRectangle(shade.Edge, ComplexGrid);
    }
    #endregion

    #region mouse overrides
    private bool _drag = false;
    private PointF _dragFrom;
    private Complex _dragBase;

    protected override Response HandleMouseDown(MouseEventArgs e)
    {
      if (e.Buttons == MouseButtons.Primary &&
           ComplexGrid.Contains(e.Location) &&
           ComplexGrip.Offset(2).Contains(e.Location))
      {
        // Only override the mouse events if the users clicks within 2 pixels of the grip.
        _drag = true;
        _dragFrom = e.Location;
        _dragBase = ((ComponentWithMouseOverrides)Owner).Z;
        return Response.Capture;
      }
      else
      {
        // Otherwise allow the default mouse handling behaviour to continue.
        _drag = false;
        _dragFrom = PointF.Empty;
        _dragBase = Complex.Zero;
        return Response.Ignored;
      }
    }
    protected override Response HandleMouseMove(MouseEventArgs e)
    {
      if (_drag && e.Buttons == MouseButtons.Primary)
      {
        // limit the dragging to within the grid. I'm not sure why.
        var location = ComplexGrid.ClosestPointIn(e.Location);

        var dx = location.X - _dragFrom.X;
        var dy = location.Y - _dragFrom.Y;
        var grid = Grid;

        grid.ControlToGrid(0, 0, out var x0, out var y0);
        grid.ControlToGrid(dx, dy, out var x1, out var y1);

        var dr = x1 - x0;
        var di = y1 - y0;

        ((ComponentWithMouseOverrides)Owner).Z = new Complex(_dragBase.Real + dr, _dragBase.Imaginary + di);
        Owner.Document?.Solution.DelayedExpire(Owner);

        return Response.Handled;
      }
      else
      {
        // Something went wrong, we're not supposed to be
        // dragging if the mouse button is not down.
        _drag = false;
        return Response.Release;
      }
    }
    protected override Response HandleMouseUp(MouseEventArgs e)
    {
      // Wipe the dragging state.
      _drag = false;
      _dragFrom = PointF.Empty;
      _dragBase = Complex.Zero;
      return Response.Release;
    }
    #endregion
  }
}
1 Like

Hi @DavidRutten
Many thanks for the clarification! Although I found a workaround to the issue by manually implementing the missing property ResponsiveState from the IResponsiveAttributes in my Attributes class :smiley:. I’ll try and switch to using ComponentWithMouseOverrides once the update is out.
Looking forward to the release!

Hi @DavidRutten
I’m back with some more plugin development related queries :smiley:

  1. Could you please elaborate on any changes made to the plugin loading mechanism? Here’s what I’ve gathered so far:
  • Plugins are now .rhp files but they still load the same way .gha files were loaded in GH1. They’re not installed as normal .rhp Rhino plugins.
  • All plugins, at least as of now, must be added to the Component folder next to all the vanilla component libraries or in a sub-directory. Currently there is no separate plugin components folder.
  • Referenced .dll assemblies must be placed in the same folder as the referencing .rhp file. This seems to be the case, but it does not work exactly this way in scenarios where components are being loaded from these referenced assemblies - I need to make a copy of the .dll as an .rhp file to make this work. Is this by design or am I missing something?
  1. The new icon system looks great! But implementing my own icons using C# seems a little scary for the volume of components I need to write. Is there already a simple workflow to use SVG assets directly or something similar?
    Also, there seems to be a bug in the rendering of icons drawn from bitmap images:
    image
    protected override IIcon IconInternal => AbstractIcon.FromBitmap(Karamba.Properties.Resources.KarambaCategoryLogo.ToEto());

@cp1

Same Problem here. “Smushing” all dlls and dependencies in one folder will blow up an resonable plugin develoment. I want to seperate my dev from the standard stuff.