Internalize custom parameter + copy / paste

Hi,

  • I’ve searched the forums and haven’t found an answer that works…

  • I have a custom data type Foo which does stuff and has member variables.

  • I’ve created a GH_Foo object, which inherits GH_Goo<Foo>, IGH_PreviewData, and GH_ISerializable.

  • This GH_Foo overrides the public override bool Write(GH_IWriter writer) and public override bool Read(GH_IReader reader) to serialize it’s value Foo object.

  • I have then created a FooParameter which inherits GH_PersistentParam<GH_Foo>.

All of this works great and lets me pass Foo around the canvas and through various components, and also make a FooParameter component.

However, when copy / pasting across definitions, or when saving / loading, the internalized data is lost, and I’m left with an empty parameter. Something either isn’t serializing right, or I’m missing a trick when it comes to internalizing the data in the parameter.

Following the answer in this, all should be well if I’ve overridden the Read / Write functions in GH_Foo, however it appears that all is not, in fact, well.

Am I missing something obvious or are there other functions that need to be overridden or other interfaces that need interfacing?

2 Likes

@tom_svilans - I’ve moved this to the Grasshopper Developer category, where it should get more attention…

1 Like

I think heriting from GH_Goo / GH_PersistentParam should be enough, not aware that you’d need to implement further interfaces.

Maybe you should post some code snippets, e.g. what happens inside the Read/Write methods.

Yeah some code would be good. The main question for starters is whether the serialisation works if you put your GH_Foo inside a generic parameter.

The reliable (de)serialisation of data was a bit of an afterthought in the GH1 api design and was never properly implemented. There’s a lot of hoops you have to jump through to make it work.

No problemo, thanks for the feedback @DavidRutten and @dsonntag. Here is the code for Read and Write:

        public override bool Write(GH_IWriter writer)
        {
            byte[] rawGuide = GH_Convert.CommonObjectToByteArray(Value.Centreline);
            writer.SetByteArray("guide", 0, rawGuide );

            writer.SetInt32("num_frames", Value.Frames.Count);

            for (int i = 0; i < Value.Frames.Count; ++i)
            {
                Plane p = Value.Frames[i].Item2;

                writer.SetPlane("frames", i, new GH_IO.Types.GH_Plane(
                    p.OriginX, p.OriginY, p.OriginZ, p.XAxis.X, p.XAxis.Y, p.XAxis.Z, p.YAxis.X, p.YAxis.Y, p.YAxis.Z));
            }

            writer.SetInt32("lcx", Value.Data.NumWidth);
            writer.SetInt32("lcy", Value.Data.NumHeight);
            writer.SetDouble("lsx", Value.Data.LamWidth);
            writer.SetDouble("lsy", Value.Data.LamHeight);
            writer.SetInt32("interpolation", (int)Value.Data.InterpolationType);
            writer.SetInt32("samples", Value.Data.Samples);

            return base.Write(writer);
        }

        public override bool Read(GH_IReader reader)
        {
            byte[] rawGuide = reader.GetByteArray("guide");
            Curve guide = GH_Convert.ByteArrayToCommonObject<Curve>(rawGuide);

            int N = reader.GetInt32("num_frames");
            Plane[] frames = new Plane[N];

            for (int i = 0; i < N; ++i)
            {
                var gp = reader.GetPlane("frames", i);
                frames[i] = new Plane(
                    new Point3d(
                        gp.Origin.x,
                        gp.Origin.y,
                        gp.Origin.z),
                    new Vector3d(
                        gp.XAxis.x,
                        gp.XAxis.y,
                        gp.XAxis.z),
                    new Vector3d(
                        gp.YAxis.x,
                        gp.YAxis.y,
                        gp.YAxis.z)
                        );
            }

            int lcx = reader.GetInt32("lcx");
            int lcy = reader.GetInt32("lcy");
            double lsx = reader.GetDouble("lsx");
            double lsy = reader.GetDouble("lsy");
            int interpolation = reader.GetInt32("interpolation");
            int samples = reader.GetInt32("samples");

            GlulamData data = new GlulamData(lcx, lcy, lsx, lsy, samples);
            data.InterpolationType = (GlulamData.Interpolation)interpolation;

            Value = Glulam.CreateGlulam(guide, frames, data);

            return base.Read(reader);
        }
    }

EDIT: Also, when copy / pasting the parameter to the same file, it ends up being a load of nulls. Same problem, I imagine…

Copy, paste, undo and redo are all basically relying on the same code as write and read. So yes, it’s the same problem.

Here’s a very basic implementation of a data type, a goo wrapper and a parameter that works:

using System;
using System.Collections.Generic;
using GH_IO.Serialization;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;

namespace GooIoTest
{
  /// <summary>
  /// A simple immutable data type with no functionality.
  /// </summary>
  public sealed class Foo
  {
    public Foo(int integer, string text)
    {
      Integer = integer;
      Text = text;
    }

    public int Integer { get; }
    public string Text { get; }

    public override string ToString()
    {
      return string.Format("[{0}] \"{1}\"", Integer, Text);
    }
  }

  /// <summary>
  /// An IGH_Goo wrapper around Foo.
  /// </summary>
  public sealed class FooGoo : GH_Goo<Foo>
  {
    #region constructors
    public FooGoo()
     : this(null)
    { }
    public FooGoo(Foo foo)
    {
      Value = foo;
    }

    public override IGH_Goo Duplicate()
    {
      // It's okay to share the same Foo instance since Foo is immutable.
      return new FooGoo(Value);
    }
    #endregion

    #region properties
    public override string ToString()
    {
      if (Value == null) return "No foo";
      return Value.ToString();
    }

    public override string TypeName => "Foo";
    public override string TypeDescription => "Pointless foo data";
    public override bool IsValid
    {
      get
      {
        if (Value == null) return false;
        if (Value.Integer < 0) return false;
        if (Value.Text == null) return false;
        return true;
      }
    }
    public override string IsValidWhyNot
    {
      get
      {
        if (Value == null) return "No data";
        if (Value.Integer < 0) return "Negative integer data";
        if (Value.Text == null) return "No text data";
        return string.Empty;
      }
    }

    public override bool CastFrom(object source)
    {
      if (source == null) return false;
      if (source is int integer)
      {
        Value = new Foo(integer, string.Empty);
        return true;
      }
      if (source is GH_Integer ghInteger)
      {
        Value = new Foo(ghInteger.Value, string.Empty);
        return true;
      }
      if (source is string text)
      {
        Value = new Foo(0, text);
        return true;
      }
      if (source is GH_String ghText)
      {
        Value = new Foo(0, ghText.Value);
        return true;
      }
      return false;
    }
    public override bool CastTo<TQ>(ref TQ target)
    {
      if (Value == null)
        return false;

      if (typeof(TQ) == typeof(int))
      {
        target = (TQ)(object)Value.Integer;
        return true;
      }
      if (typeof(TQ) == typeof(GH_Integer))
      {
        target = (TQ)(object)new GH_Integer(Value.Integer);
        return true;
      }

      if (typeof(TQ) == typeof(double))
      {
        target = (TQ)(object)Value.Integer;
        return true;
      }
      if (typeof(TQ) == typeof(GH_Number))
      {
        target = (TQ)(object)new GH_Number(Value.Integer);
        return true;
      }

      if (typeof(TQ) == typeof(string))
      {
        target = (TQ)(object)Value.Text;
        return true;
      }
      if (typeof(TQ) == typeof(GH_String))
      {
        target = (TQ)(object)new GH_String(Value.Text);
        return true;
      }

      return false;
    }
    #endregion

    #region (de)serialisation
    private const string IoIntegerKey = "Integer";
    private const string IoTextKey = "Text";
    public override bool Write(GH_IWriter writer)
    {
      if (Value != null)
      {
        writer.SetInt32(IoIntegerKey, Value.Integer);
        if (Value.Text != null)
          writer.SetString(IoTextKey, Value.Text);
      }
      return true;
    }
    public override bool Read(GH_IReader reader)
    {
      if (!reader.ItemExists(IoIntegerKey))
      {
        Value = null;
        return true;
      }

      int integer = reader.GetInt32(IoIntegerKey);
      string text = null;

      if (reader.ItemExists(IoTextKey))
        text = reader.GetString(IoTextKey);

      Value = new Foo(integer, text);

      return true;
    }
    #endregion
  }

  public sealed class FooParameter : GH_PersistentParam<FooGoo>
  {
    public FooParameter()
    : this("Foo", "Foo", "A collection of Foo data", "Foo", "Bar")
    { }
    public FooParameter(GH_InstanceDescription tag) : base(tag) { }
    public FooParameter(string name, string nickname, string description, string category, string subcategory)
      : base(name, nickname, description, category, subcategory) { }

    public override Guid ComponentGuid => new Guid("{606C9679-C36C-48B1-A547-22B68EE8A0A1}");
    protected override GH_GetterResult Prompt_Singular(ref FooGoo value)
    {
      return GH_GetterResult.cancel;
    }
    protected override GH_GetterResult Prompt_Plural(ref List<FooGoo> values)
    {
      return GH_GetterResult.cancel;
    }
  }
}
4 Likes

Thanks very much for this! I will give it a try.

So… copied your example, still didn’t work.

Then realized that in Write() I was calling writer.SetByteArray(string key, int index, byte[] bArray) whereas in Read() I was calling reader.GetByteArray(string key), the difference being the int index in the writer.

Removed this, and now it seems to serialize / deserialize correctly. Another case of code blindness :see_no_evil:

Apologies for wasting your time and thanks for your help!

Great example!

Currently the data types of the Foo fields are int and string and therefore at Read and Write functions one can use reader.GetInt32() / reader.GetString() and writer.SetInt32() / writer.SetString() respectively. How about having Curve, Brep, Mesh or even List<Curve>, Curve[], List<Brep>, Brep[], List<Mesh>, Mesh[] as data types in the Foo class? Thanks a lot!