Python to C# Conversion error- Pick point and manipulate Gumball

I found this Python version of implementing Gumball in Grasshopper.

import Rhino
from Rhino.Display import PointStyle
from Rhino.Input.Custom import  GetTransform, PickContext, PickStyle 
from Rhino.Input import GetResult
from Rhino.Commands import Result
from Rhino.UI.CursorStyle import Default
from Rhino.UI.Gumball import GumballObject, GumballDisplayConduit, GumballAppearanceSettings, GumballMode
from Rhino.Geometry import Transform, Line, Point3d,Plane, Vector3d
from Rhino.RhinoDoc import ActiveDoc
import scriptcontext as sc
from System.Windows.Forms import MessageBox
from System.Drawing import Color
from System.Windows.Forms.Cursors import Arrow

def gumball_appearance_settings():
    """Returns gumball appearance settings with only translation switched on"""
    gas = GumballAppearanceSettings()
    gas.ScaleXEnabled = False
    gas.ScaleYEnabled = False
    gas.ScaleZEnabled = False
    gas.RotateXEnabled = False
    gas.RotateYEnabled = False
    gas.RotateZEnabled = False
    gas.MenuEnabled = False
    gas.RelocateEnabled = False
    return gas

class CustomGetTransform(GetTransform):
    """
    CustomGet class derived from GetTransform
    """
    def __init__(self, pts_to_select_from, display_conduit):
        self.pts_to_select_from = pts_to_select_from
        self.dc = display_conduit
        self.current_sel_index = None
        #info is a dictionary storing (index of selected point, transform) tuples
        self.info = {}

    def CalculateTransform(self,viewport,point):
        return self.dc.TotalTransform

    def OnMouseDown(self, e):
        #initial mouse down event
        if self.dc.PickResult.Mode != GumballMode.None:
            return
        self.dc.PickResult.SetToDefault()
        picker = PickContext()
        picker.View = e.Viewport.ParentView
        picker.PickStyle = PickStyle.PointPick
        xform = e.Viewport.GetPickTransform(e.WindowPoint)
        picker.SetPickTransform(xform)
        # Point selection
        for index, pt in enumerate(self.pts_to_select_from):
            if picker.PickFrustumTest(pt)[0]:
                self.current_sel_index = index
                if index not in self.info:
                    self.info[index] = Transform.Identity
                break
        # Gumball
        picker.PickLine = e.Viewport.GetFrustumLine(e.WindowPoint.X, e.WindowPoint.Y)[1]
        picker.UpdateClippingPlanes()
        self.dc.PickGumball(picker, self)

    def update_selected_point(self,xform):
        """ Update the selected point and the info dictionary"""
        _pt  = self.pts_to_select_from[self.current_sel_index]
        _pt.Transform(xform)
        self.info[self.current_sel_index] *= xform

    def OnMouseMove(self, e):
        """ Updates the Gumball in the display conduit"""
        if self.dc.PickResult.Mode == GumballMode.None: 
            return
        res, world_line = e.Viewport.GetFrustumLine(e.WindowPoint.X, e.WindowPoint.Y)
        if not res: world_line = Line.Unset
        rc =  self.dc.UpdateGumball(e.Point, world_line)
        if rc:
            GetTransform.OnMouseMove(self,e)

    def MoveGumball(self):
        if self.dc.PreTransform != Transform.Identity:
            self.HaveTransform = True
            self.Transform = self.dc.PreTransform
        self.SetBasePoint(self.dc.BaseGumball.Frame.Plane.Origin, False)
        if self.Transform != Transform.Identity:
            self.update_selected_point(self.Transform)
        return self.Get(True)

    def OnDynamicDraw(self,e):
        for i in self.info.keys():
            e.Display.DrawPoint(self.pts_to_select_from[i],PointStyle.X, 5, Color.Green)

def manipulate_points(pts_to_select_from):
    
    conduit = GumballDisplayConduit()
    base_gumball = GumballObject()
    cgt = CustomGetTransform(pts_to_select_from, conduit)
    cgt.SetCursor(Default)
    cgt.Enabled = True

    while 1:
        if cgt.current_sel_index is not None:
            current_sel_pt = pts_to_select_from[cgt.current_sel_index]
            if conduit.PickResult.Mode == GumballMode.None:
                base_gumball.SetFromPlane(Plane(current_sel_pt, Vector3d.ZAxis))
            conduit.SetBaseGumball(base_gumball, gumball_appearance_settings())
            conduit.Enabled = True
            cgt.dc = conduit
            cgt.SetCommandPrompt("Drag gumball or select another point. (<ESC> to exit)")
            cgt.MoveGumball()
            conduit.Enabled = False
        else:
            cgt.SetCommandPrompt("Select grasshopper points. (<ESC> to exit)")
            cgt.Get(True)
        sc.doc.Views.Redraw()
        if cgt.CommandResult() != Result.Success:
            cgt.Enabled = False
            return cgt.info
        if cgt.Result() == GetResult.Point:
            # Update base gumball
            gb_frame = conduit.Gumball.Frame
            base_frame = base_gumball.Frame
            base_frame.Plane = gb_frame.Plane
            base_gumball.Frame = base_frame

def reset_toggle():
    """Turns off toggle (on) connected to this component"""
    toggle = ghenv.Component.Params.Input[1].Sources[0]
    toggle.Value = False
    toggle.ExpireSolution(True)


if reset and "get_result" in sc.sticky:
    sc.sticky["get_result"].clear()

if on:
    #MessageBox Prompt
    MessageBox.Show("Pick points in the rhino window")
    #Can Ignore the MessageBox, not That Necessary
    # Call manipulate_points
    sc.sticky["get_result"] =  manipulate_points(pts)
    reset_toggle()

if "get_result" in sc.sticky:
    for index, xform in sc.sticky["get_result"].iteritems():
        pts[index].Transform(xform)
a = pts

I translated the Code to C# here

using System;
using System.Collections;
using System.Collections.Generic;

using Rhino;
using Rhino.Geometry;

using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;

using System.Linq;
using Grasshopper.Kernel.Special;
using Grasshopper.Kernel.Types.Transforms;
using Rhino.Input.Custom;
using Rhino.Commands;
using Rhino.Display;
using Rhino.Input;
using Rhino.UI;
using System.Windows.Forms;
using System.Drawing;
using Rhino.UI.Gumball;

/// <summary>
/// This class will be instantiated on demand by the Script component.
/// </summary>
public class Script_Instance : GH_ScriptInstance
{
#region Utility functions
  /// <summary>Print a String to the [Out] Parameter of the Script component.</summary>
  /// <param name="text">String to print.</param>
  private void Print(string text) { /* Implementation hidden. */ }
  /// <summary>Print a formatted String to the [Out] Parameter of the Script component.</summary>
  /// <param name="format">String format.</param>
  /// <param name="args">Formatting parameters.</param>
  private void Print(string format, params object[] args) { /* Implementation hidden. */ }
  /// <summary>Print useful information about an object instance to the [Out] Parameter of the Script component. </summary>
  /// <param name="obj">Object instance to parse.</param>
  private void Reflect(object obj) { /* Implementation hidden. */ }
  /// <summary>Print the signatures of all the overloads of a specific method to the [Out] Parameter of the Script component. </summary>
  /// <param name="obj">Object instance to parse.</param>
  private void Reflect(object obj, string method_name) { /* Implementation hidden. */ }
#endregion

#region Members
  /// <summary>Gets the current Rhino document.</summary>
  private readonly RhinoDoc RhinoDocument;
  /// <summary>Gets the Grasshopper document that owns this script.</summary>
  private readonly GH_Document GrasshopperDocument;
  /// <summary>Gets the Grasshopper script component that owns this script.</summary>
  private readonly IGH_Component Component;
  /// <summary>
  /// Gets the current iteration count. The first call to RunScript() is associated with Iteration==0.
  /// Any subsequent call within the same solution will increment the Iteration count.
  /// </summary>
  private readonly int Iteration;
#endregion

  /// <summary>
  /// This procedure contains the user code. Input parameters are provided as regular arguments,
  /// Output parameters as ref arguments. You don't have to assign output parameters,
  /// they will have a default value.
  /// </summary>
  private void RunScript(List<Point3d> pts, bool on, object clear, ref object A)
  {
    List<Line> outLines = new List<Line>();
    if (on)
    {
      stickyData = ManipulatePoints(pts);

      var toggle = this.Component.Params.Input[1].Sources[0];
      ((GH_BooleanToggle) toggle).Value = false;
      toggle.ExpireSolution(true);
    }

    if (stickyData.Count > 0)
    {
      if ((bool) clear) stickyData.Clear();
      foreach (var i in stickyData.Keys)
        pts[i].Transform(stickyData[i]);
    }
    A = pts;
  }

  // <Custom additional code> 
  Dictionary<int, Transform> stickyData = new Dictionary<int, Transform>();

  private GumballAppearanceSettings CustomGumballAppearanceSettings()
  {
    //Return Gumball Appearance Settings with only Translation
    var gas = new GumballAppearanceSettings
      {
        RotateXEnabled = false,
        RotateYEnabled = false,
        RotateZEnabled = false,
        ScaleXEnabled = false,
        ScaleYEnabled = false,
        ScaleZEnabled = false,
        TranslateXEnabled = true,
        TranslateXYEnabled = true,
        TranslateYEnabled = true,
        TranslateYZEnabled = true,
        TranslateZEnabled = true,
        TranslateZXEnabled = true,
        MenuEnabled = false,
        RelocateEnabled = false
        };
    return gas;
  }

  class CustomGetTransform : GetTransform
  {
    public GumballDisplayConduit lineDc;
    private readonly List<Point3d> ptsToSelectFrom;
    public int currentSelIndex;
    public readonly Dictionary<int, Transform> info;

    public CustomGetTransform(GumballDisplayConduit inLineDc, List<Point3d> inPtsToSelectFrom)
    {
      lineDc = inLineDc;
      ptsToSelectFrom = inPtsToSelectFrom;
      currentSelIndex = -1;
      info = new Dictionary<int, Transform>();
    }

    public override Transform CalculateTransform(RhinoViewport viewport, Point3d point)
    {
      // do we want this first line??
      //if (lineDc.InRelocate) return lineDc.PreTransform;
      return lineDc.TotalTransform;
    }

    protected override void OnMouseDown(GetPointMouseEventArgs e)
    {
      //initial mouse down event
      if (lineDc.PickResult.Mode != GumballMode.None)
        return;

      lineDc.PickResult.SetToDefault();

      var picker = new PickContext
        {
          View = e.Viewport.ParentView,
          PickStyle = PickStyle.PointPick
          };

      var xform = e.Viewport.GetPickTransform(e.WindowPoint);
      picker.SetPickTransform(xform);

      //Point Selection

      for (int i = 0; i < ptsToSelectFrom.Count; i++)
      {
        double depth;
        double distance;
        if (picker.PickFrustumTest(ptsToSelectFrom[i], out depth, out distance))
        {
          currentSelIndex = i;
          if (!info.ContainsKey(i))
            info.Add(i, Transform.Identity);
          break;
        }
      }
      //Gumball
      Line pickLine;
      e.Viewport.GetFrustumLine(e.WindowPoint.X, e.WindowPoint.Y, out pickLine);

      picker.PickLine = pickLine;
      picker.UpdateClippingPlanes();
      lineDc.PickGumball(picker, this);
    }

    private void updateSelectedPoint(Transform xform)
    {
      //Update the Selected Point and info Dictionary
      var pt = ptsToSelectFrom[currentSelIndex];
      pt.Transform(xform);
      info[currentSelIndex] *= xform;
    }

    protected override void OnMouseMove(GetPointMouseEventArgs e)
    {
      //Update the Gumball in the Display Conduit
      if (lineDc.PickResult.Mode == GumballMode.None) return;
      //lineDc.CheckShiftAndControlKeys();
      Line worldLine;
      var res = e.Viewport.GetFrustumLine(e.WindowPoint.X, e.WindowPoint.Y, out worldLine);
      if (!res)
        worldLine = Line.Unset;
      var rc = lineDc.UpdateGumball(e.Point, worldLine);
      if (rc)
        base.OnMouseMove(e);
    }




    public GetResult MoveGumball()
    {
      // Get point on a MouseUp event
      if (lineDc.PreTransform != Transform.Identity)
      {
        HaveTransform = true;
        Transform = lineDc.PreTransform;
      }
      SetBasePoint(lineDc.BaseGumball.Frame.Plane.Origin, false);

      // V5 uses a display conduit to provide display feedback
      // so shaded objects move
      //ObjectList.DisplayFeedbackEnabled = true;
      if (this.Transform != Transform.Identity)
      {
        //ObjectList.UpdateDisplayFeedbackTransform(Transform);
        updateSelectedPoint(this.Transform);
      }

      // Call Get with mouseUp set to true
      var rc = this.Get(true);

      // V5 uses a display conduit to provide display feedback
      // so shaded objects move
      //ObjectList.DisplayFeedbackEnabled = false;
      return rc;
    }

    protected override void OnDynamicDraw(GetPointDrawEventArgs e)
    {
      foreach (var i in info.Keys)
        e.Display.DrawPoint(ptsToSelectFrom[i], PointStyle.X, 5, System.Drawing.Color.Green);
    }
  }

  private Dictionary<int, Transform> ManipulatePoints(List<Point3d> ptsToSelectFrom)
  {
    var conduit = new GumballDisplayConduit();
    var baseGumball = new GumballObject();
    var cgt = new CustomGetTransform(conduit, ptsToSelectFrom);
    cgt.SetCursor(CursorStyle.Default);
    cgt.lineDc.Enabled = true;

    while (true)
    {
      if (cgt.currentSelIndex >= 0)
      {
        var currentSelPt = ptsToSelectFrom[cgt.currentSelIndex];
        if (conduit.PickResult.Mode == GumballMode.None)
          baseGumball.SetFromPlane(new Plane(currentSelPt, Vector3d.ZAxis));
        conduit.SetBaseGumball(baseGumball, CustomGumballAppearanceSettings());
        conduit.Enabled = true;
        cgt.lineDc = conduit;
        cgt.SetCommandPrompt("Drag gumball or select another point. (<ESC> to exit)");
        cgt.MoveGumball();
        conduit.Enabled = false;
      }
      else
      {
        cgt.SetCommandPrompt("Select grasshopper points. (<ESC> to exit)");
        cgt.Get(true);
      }
      RhinoDoc.ActiveDoc.Views.Redraw();
      if (cgt.CommandResult() != Result.Success)
      {
        cgt.lineDc.Enabled = false;
        return cgt.info;
      }
      if (cgt.Result() == GetResult.Point)
      {
        // update base gumball
        var gbFrame = conduit.Gumball.Frame;
        var baseFrame = baseGumball.Frame;
        baseFrame.Plane = gbFrame.Plane;
        baseGumball.Frame = baseFrame;
      }

    }
  }

  // </Custom additional code> 
}

The Code seems to be working all good and there is no exception thrown by the component.
I can Pick the point successfully, but when I move the Gumball the position of Point does not update.


Here is the Grasshopper script attached as well. Python to C# Gumball manipulate.gh (13.8 KB)

Can someone help me pointing out what could be the error. Thanks.


Point3d is a structure not a class (value type rather than reference type), so you need to change this for-loop:
foreach (var i in stickyData.Keys)
  pts[i].Transform(stickyData[i]);

to something like this:

foreach (var i in stickyData.Keys)
{
  var pt = pts[i];
  pt.Transform(stickyData[i]);
  pts[i] = pt;
}

Gumball.gh (10.8 KB)

@Mahdiyar Thanks a lot!
Another question I have, is there a way to save the new position of points after I close the Grasshopper/ Rhino file within the code so that next Time I open my Grasshopper file, it shows the latest state of the points.
Any input will be helpful to get me started. Thanks!

Great! I will look at this. Thanks once again.