Saving geometry for easy access in C#

Hi everyone!

I’m creating some custom components in c# in visual studio and I’m currently trying to create a component where the user gets to pick a house type from a list of 5 types (A-E). I’ve created a house class where I store the properties of the house and the user can later “explode” the object to get back all the properties of the selected house.

What I’m thinking is that it would be neat to render low level version of the house when the user picks the type by default, inside the component itself, instead of having to reference the geometry and interpolate which I’m doing right now.

What do you think is the best way of doing this? Should I save the data to a .json file somehow and reference that file in c# or is there a way of actually saving a brep as pure c# code in Rhinocommon?

Thank you!
Erik

My next version of Peacock has several assets libraries, as default geometries. What I’ve done is serialize those geometries in an XML (it could also be a JSON, but I already had experience with XML) and load that files as resources in Visual Studio. When the plugin is loaded, I deserialize all the libraries and store the geometries in their respective object galleries. Some components take care of finding, cloning and using them.

Consider loading them only when you need them (like instantiating a component that requires them) if they are too heavy to be loaded all at once when starting GH.

OpenNurbs/Rhino allows to serialize GeometryBases in binary and vice versa. But for ease of use, you can use the GH methods:
Grasshopper.Kernel.GH_Convert.CommonObjectToByteArray() and Grasshopper.Kernel.GH_Convert.ByteArrayToCommonObject()

Hi Dani!

Thank you for your reply, It was very helpful. I’ve been thinking along the same lines with loading all the objects at once (they’re not that heavy), but I just need to get the serialisation to work first. I tried using CommonObjectToByteArray() and ByteArrayToCommonObject() in a c# component inside grasshopper and that works really well for a brep:

When I try using the same method in visual studio I run into a problem though. I think the byte[.] is fine and that it’s reading it correctly, but I haven’t worked with it before so I’m not sure. Here’s what it looks like and the code for it anyway:

public static class ReadGeometry
    {
        public static Brep ReadHouseGeometry(string type)
        {
            string pt1 = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            string path = pt1 + "\\A1.txt"; //only a placeholder for now as I have only one geometry in binary
            byte[\*] A1 = System.IO.File.ReadAllBytes(path);
            GeometryBase geometry = GH_Convert.ByteArrayToCommonObject<GeometryBase>(A1);
            return geometry as Brep;
        }
    }

The problem is that the GeometryBase object becomes null on the ByteArrayToCommonObject() line. Not sure if this is a problem with the byte[.] or some reference missing.

Thanks again!

Erik

1 Like

I have the same problem do you solved this problem ?

Hi @flokart,

Using File3dm might be a better approach.

– Dale

1 Like

Thanks for the tip,

i invested more time to find the problem and its not rhino.
If the System.Byte gets insert in a sqlite database it gets crop/ or compressed to 13characters.

Hello @flokart,

I’m not sure if you’ve already solved this issue but I thought I’d post my solution here anyway for future reference.

I think what I did wrong in my last post was that I tried to input the System.Byte[.] into Convert.ByteArrayToCommonObject(), but it actually takes the path to the file containing the byte array. At least that works for me. If your System.Byte[.] is just a variable (and not a file), you could still write it to a temp file and use the path to that to use the method.

This is what my code looks like:

public static Brep ReadGeometry(string type)
        {
            string fileName = ReadResourceFile("Engine.Resources." + type + ".txt");
            GeometryBase geometry = GH_Convert.ByteArrayToCommonObject<GeometryBase>(System.Convert.FromBase64String(fileName));
            return geometry as Brep;
        }

Good luck and feel free to ask more questions if it is not clear.

BR
Erik

5 Likes

@erikforsberg95 Isn’t there a way to do that without saving a temp file? because writing to a file, in my case at least, kind of defeats the purpose of being able to internalize the data.

Hello @hebame164!

I had another look at this and turns out I was wrong before when I said you need a temp file. I think you can use Convert.FromBase64String() and pass in a path, but it works with the actual byte array as well. See the attached file for some guidance on how to do this. Hope this helps and let me know if you still have problems.

Best,
Erik
base64_back_and_forth.gh (4.2 KB)

2 Likes

Thank you very much for your reply!
I am using the same method, the geometry is read while I am inside the file, but if I save it and reopen it again, it disappears…
But at least now I know there is nothing wrong with this method thanks to you :slight_smile:

Hi,

it seems the problem is that GeometryBase is an abstract class. The only thing you do is to convert your geometry into an array of bytes. But of course it matters which type you are actually serializing.
Serializing a NurbsCurve yields a different bytearray with a different length, as a NurbsSurface would have. So how should the deserializer know how long this bytearray is if it doesn’t know what concrete implementation it is?

What I would do, if I want to keep it in one file, is the following: I would serialize a string telling what the following bytearray is of type and how long each bytearray is. Kind of a header to the data. Then I would just dump the byte arrays inside the file.When you deserialize you know what the following bytes are representing, so that you can deserialize it correctly. Does this make sense or is this explanation too abstract/difficult?

1 Like


from what I’ve seen using
Grasshopper.Kernel.GH_Convert.CommonObjectToByteArray(x);
the only ‘filtering’ necessary is the input as the method doesn’t accept GH_object, but geometry types.
feeding in meshes, lines, curves, breps etc seem to work just fine.

interpreter code:
A = GH_Convert.ByteArrayToCommonObject<GeometryBase>(System.Convert.FromBase64String(x));

1 Like

Oh okay, so there is already a built-in feature… For own or other types you need to write a converter for yourself. But its really simple. You really just need to know what the byte-array represents…

See:

1 Like

Have you tried it in VS instead of c# script like in the 3 post ?

I think I did, and it worked out fine, but I admit it’s been a while since…

@flokart @erikforsberg95 I just retested it, I wasn’t able to find the old file. however, it works fine in VS.

sender code:

        protected override void SolveInstance(IGH_DataAccess DA)
        {
            Brep b = null;
            if (!DA.GetData(0, ref b)) return;
            byte[] bytes = Grasshopper.Kernel.GH_Convert.CommonObjectToByteArray(b);
            string str = Convert.ToBase64String(bytes);
            DA.SetData(0, str);
        }

receivers code:

protected override void SolveInstance(IGH_DataAccess DA)
        {
            string str = null;
            if (!DA.GetData(0, ref str)) return;
            GeometryBase b =  GH_Convert.ByteArrayToCommonObject<GeometryBase>(System.Convert.FromBase64String(str));
            DA.SetData(0, b);
        }

I guess if you want to load from a file, you first need to convert it into base64 string, but havent’t tested this yet. from my point of view if it’s simple geometry, I’d save the string. however, I am not a professional programmer, so my proposition might be ridicoulos.

Ok, I have tested it now and you can hardcode base64 strings containing geometrybase or read it from a file and have it given out by your gh node. I have chosen the lazy plaintext txt file to save the geometry though, no fancy stuff. and BTW, too large geometry strings brake the panel node, however it’s stream function still works.

hope this helps

Here is a Console App which basically shows what I mean on a lower level. Once you understand the concept you can adapt this logic to a more flexible persistence within the GH Serializer. Works without Rhino.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;

namespace SerializeDeserializerDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // THIS IS A DEMO SCRIPT WITH NO CHECKS!

            // Create fake data
            Console.WriteLine("Creating some fake geometrical data...");
            var data = new List<MurbsCurve>();
            data.Add(MurbsCurve.CreateRandomCurve());
            data.Add(MurbsCurve.CreateRandomCurve());
            data.Add(MurbsCurve.CreateRandomCurve());
            Console.WriteLine("Fake geometrical data created!\n");
            

            // Get their byte arrays
            Console.WriteLine("Extract their byte arrays...");
            List<byte[]> byteArrays = new List<byte[]>(data.Count);
            for (int i = 0; i < 3; i++)
            {
                byteArrays.Add(data[i].SerializeToByteArray());
            }
            Console.WriteLine("Byte arrays extracted!\n");


            // Create file header => ObjectCount|ObjectType|ByteLength|ObjectType|ByteLength|{...}
            Console.WriteLine("Create header file");
            var sb = new StringBuilder();
            sb.Append($"{byteArrays.Count}");
            for (int i = 0; i < byteArrays.Count; i++)
            {
                sb.Append($"|MurbsCurve|{byteArrays[i].Length}");                
            }           
            string header = sb.ToString();
            Console.WriteLine(header);
            Console.WriteLine("header file created!\n");


            // Create a file (simplifies the GH part here)
            Console.WriteLine("Persisting data into a file...");
            string path = @"C:\Temp\somerandom.bin";
            using (var stream = File.Open(path, FileMode.Create))
            using (BinaryWriter writer = new BinaryWriter(stream))
            {
                writer.Write(header);
                byteArrays.ForEach(b => writer.Write(b));
            }
            Console.WriteLine($"File written @ {path}");


            // Read file 
            Console.WriteLine($"Reading file from @ {path}...");
            
            List<MurbsCurve> dataReceived = new List<MurbsCurve>();
            
            using (var stream = File.Open(path, FileMode.Open))
            using (BinaryReader reader = new BinaryReader(stream))
            {
                header = reader.ReadString();
                string[] headerInfo = header.Split('|');
                int objectCount = int.Parse(headerInfo[0]);
                int indexInfo = 1;
                for (int i = 0; i < objectCount; i++)
                {
                    string geomType = headerInfo[indexInfo];
                    indexInfo++;
                    int byteCount = int.Parse(headerInfo[indexInfo]);
                    indexInfo++;
                    byte[] byteArray = reader.ReadBytes(byteCount);
                    if (geomType == "MurbsCurve")
                    {
                        dataReceived.Add(byteArray.Deserialize<MurbsCurve>());
                    }                   
                }
            }
            Console.WriteLine($"Reading file completed!\n");


            // Assert
            ConsoleColor previousColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine($"Asserting...");

            Console.WriteLine($"Is Curve count equal?\n" +
                $"  Expect {data.Count} is {dataReceived.Count} -> {data.Count == dataReceived.Count}\n");

            Console.WriteLine($"Has the first curve the same amount of points?\n" +
                $"  Expect {data[0].Points.Length} is {dataReceived[0].Points.Length} -> {data[0].Points.Length == dataReceived[0].Points.Length}\n");

            Console.WriteLine($"Has the first curve the same first point?");
            Console.WriteLine($"  Expect X {data[0].Points[0].X} is {dataReceived[0].Points[0].X} -> {data[0].Points[0].X == dataReceived[0].Points[0].X}");
            Console.WriteLine($"  Expect Y {data[0].Points[0].Y} is {dataReceived[0].Points[0].Y} -> {data[0].Points[0].Y == dataReceived[0].Points[0].Y}");
            Console.WriteLine($"  Expect Z {data[0].Points[0].Z} is {dataReceived[0].Points[0].Z} -> {data[0].Points[0].Z == dataReceived[0].Points[0].Z}");

            Console.WriteLine($"\nAssertion complete!");
            Console.ForegroundColor = previousColor;
        }


   
    }
    /// <summary>
    /// Some nested class, acting as a fake curve
    /// </summary>
    [Serializable]
    public class MurbsCurve
    {
        private static Random _random = new Random(2324);
        public Boint3d[] Points { get; } = new Boint3d[0];
        public int Order { get; }

        public MurbsCurve()
        {
        }

        private MurbsCurve(int order)
        {
            Points = new Boint3d[order];
            Order = order;
        }

        /// <summary>
        /// Create a random MurbsCurve
        /// </summary>
        /// <param name="order"></param>
        /// <returns></returns>
        public static MurbsCurve CreateRandomCurve(int order = 4)
        {
            MurbsCurve curve = new MurbsCurve(order);
            
            for (int i = 0; i < order; i++)
            {
                curve.Points[i] = new Boint3d(
                    _random.NextDouble(),
                    _random.NextDouble(),
                    _random.NextDouble()); 
            }
            return curve;
        }
    }

    /// <summary>
    /// Some Point struct.
    /// </summary>
    [Serializable]
    public struct Boint3d
    {
        public double X;
        public double Y;
        public double Z;
       
        public Boint3d(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }
        

    /// <summary>
    /// Adds some extension methods for serialization
    /// </summary>
    public static class SerializationHelper
    {
        public static byte[] SerializeToByteArray<T>(this T obj) 
        {
            if (obj == null)
            {
                return null;
            }
            var formatter = new BinaryFormatter();
            using (var stream = new MemoryStream())
            {
                formatter.Serialize(stream, obj);
                stream.Seek(0, SeekOrigin.Begin);
                return stream.ToArray();
            }
        }

        public static T Deserialize<T>(this byte[] byteArray)
        {
            if (byteArray == null)
            {
                return default(T);
            }
            var formatter = new BinaryFormatter();
            using (var stream = new MemoryStream(byteArray))
            {
                return (T)formatter.Deserialize(stream);
            }
        }
    }
}

2 Likes

haha, I absolutely love the Boint3d and the MurbsCurve :grin:

1 Like