接下来的两章,我们来介绍一下在之前章节尚未介绍到的,但却在Grasshopper中占据极其重要地位的另一批我们早就虎视眈眈但却还没想到理由要去触碰的电池们(左侧红色框指示):
是的,就是这一些带黑底的电池们
经过仔细思考,笔者还是决定将该部分内容拆分为上下两篇。
- 上篇主要讲为什么要存在自定义类型数据,
IGH_Goo
接口对于我们日常使用 Rhino + Grasshopper 这两个工具而言,到底解决了什么问题。 - 下篇则是直接介绍如何制作一个自己的数据类型来实现这个接口,并且借助 Grasshopper 的一些基础类,来完成对这个自定义数据类型的一些简单电池的封装,以及如何结合我们之前学习的
GH_Component
电池来完成特定的场景,包括不限于:- 自定义数据类型电池
- 制定隐式数据转换规则(从
string
读取数据转换为自己的类型) - 内嵌至
GH_Component
中实现自定义类型的隐式转换
好了,下面就正式开始我们的上篇!
正如我们在前文中提到的,这些带黑底电池与其他电池不同有着明显的不同:其他电池有入口出口的详细说明文字,但这些电池却仅仅提供了出入口,似乎没有说明一般。
我们对它们的使用也有着明显的风格区别:平时我们最多的用途就是用它们来 拾取Rhino中既有的几何物件,亦或是只用它们来做数据的中转站,让电池之间的连线不至于那么长。
事实上,它们都没有数据处理功能,说的更具体一点,这些电池中,没有类似于我们之前写的电池的 SolveInstance
的功能。
“那它有个锤子用?”
啊这… 存储数据本身就是一个很重要的功能,比如说笔者最喜欢的一个功能就是在Rhino中拾取曲线之后,通过右键菜单中的 Internalize 选项,将这个拾取的曲线的数据与Rhino数据断开连接,这样这样我们把这个 .gh 文件发送给别人进行分享的时候,无需附带原来的Rhino的 .3dm 文件,其他人就可以看到这条曲线了。
也就是说,这些电池的最重要的功能就是 存储数据至 .gh 文件中
我们都知道,Grasshopper是“参数化”建模工具,但是这个参数化最初还是需要几个既定的基点的,也就是说,Grasshopper的数据流总归是需要一个 数据起点。而这帮子黑乎乎的电池就充当了这样一个重任 —— 将数据的起点能够保存到 .gh 文件中,让文件在分享过程中,数据不至于丢失。
IGH_Goo
的作用
要实现数据保存至 .gh 文件中,必须 实现 IGH_Goo
接口。且只有实现了这个接口的数据,才能在Grasshopper画布里的电池之间进行数据传递时,不出现奇奇怪怪的问题。
有人肯定会提出反对意见:
并且可以给出代码:
// 一个简单的L型钢定义
public class LSection
{
public double Width { get; set; }
public double Height { get; set; }
public double Thickness { get; set; }
public LSection(double w, double h, double t)
{
if (w > h) // 一个简单的判断,让width永远大于height,方便操作。
{
Width = w; Height = h; Thickness = t;
}
else
{
Width = h; Height = w; Thickness = t;
}
}
public bool IsEqualLeg => Width == Height;
// 重写ToString方法,在连接到Panel电池时可直接获取其数据。
public override string ToString()
{
return $"L-Shaped Section:{Width}x{Height}x{Thickness}";
}
}
// 制作L型钢的电池
public class CreateLShapedSection : GH_Component
{
public CreateLShapedSection() :
base("Create L Section", "L Section", "Description", "Params", "DigitalCrab") { }
static Guid _cid = Guid.Parse("{8C0B597B-00F1-4C0F-9B8A-E5A2B740C912}");
public override Guid ComponentGuid => _cid;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddNumberParameter("Width", "W", "", GH_ParamAccess.item);
pManager.AddNumberParameter("Height", "H", "", GH_ParamAccess.item);
pManager.AddNumberParameter("Thickness", "T", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddGenericParameter("L型钢截面", "L", "", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
var w = 0.0; var h = 0.0; var t = 0.0;
if (!DA.GetData(0, ref w) ||
!DA.GetData(1, ref h) ||
!DA.GetData(2, ref t))
return;
DA.SetData(0, new LSection(w, h, t));
}
}
上面这个电池可以很好地运行:
由动图可以看到,即便是自定义的类型,仍然是可以通过在 Grasshopper 中使用 Data
电池来进行一个临时性的数据中转,也可以调用 Internalize 菜单项将电池之间的线断开,保持数据在 Data
电池中。我们的 LSection
数据类型也能很好地在被连入 Panel
电池时,调用其 ToString()
方法,展示他的三项属性值。( Data
电池位置附下图)
这一切看起来都很美好。看起来没问题的数据,但事实真的是这样的吗?
既然笔者都这么说了,那肯定不是了嘛…… 这种情况下,至少会出现两类问题:
- 复制粘贴时,数据无法正常被粘贴
- .gh 文件保存/打开时,数据会出现丢失
其实这两类问题可以归结为一个问题:“序列化/反序列化”问题。
“序列化/反序列化”问题
我们对断开连接后的数据,但凡进行一个简单的电池的“复制粘贴”操作,这“临时”保存在 Data
电池中的L型钢数据就 灰飞烟灭 了:
答案是 —— 丢失了。因为Grasshopper不知道如何这类数据的“序列化/反序列化”操作,所以这数据就直接没了。
Grasshopper的“复制粘贴”操作本质上是两段操作:1)序列化数据至内存,进行“复制”;2)反序列化内存数据至画布,进行“粘贴”。由于LSection
数据类型未能实现接口IGH_Goo
,导致在进行数据序列化的时候失败,进而粘贴肯定就是失败的啦。
同样的,如果我们保存这个文件,再打开,也会出现数据丢失。因为文件的保存和打开也是序列化相关的操作,大家可以按这代码尝试一波,这里就不录制gif动图了。
小结
IGH_Goo
接口的出现,就是为了避免Grasshopper在操作数据时出现数据丢失的问题而存在的。只有实现了 IGH_Goo
这个接口的类,在作为数据存储于 Grasshopper 中才会完整地保存,在 Grasshopper 中被执行序列化/反序列化操作时才能够做到数据安全。
在 Rhino + Grasshopper 这个参数化工具的组合越来越被认可,由其延伸出的与各类其他软件的交互越来越多的情况下,我们对自定义数据类型的需求是必定存在的。从而,我们也很有必要去认识到将自定义数据类型在 Grasshopper 这套框架下的序列化方法实现,对于数据在不同平台之间的传递也是一个较为可靠的保证。
自定义类实现 IGH_Goo
接口
首先需要说明的是,因为 IGH_Goo
是面向数据类的一个接口,它本身与 Grasshopper 电池 无关。
所以,【基础13】本文只会介绍如何写一个数据类型,而基于这个实现了 IGH_Goo
的数据类型而实现的相关电池的编写,将会放在下一篇中叙述。
首先我们来观察一下,在 IGH_Goo
接口中,定义了哪些必须实现的接口函数:
public interface IGH_Goo : GH_ISerializable
{
bool IsValid { get; }
string IsValidWhyNot { get; }
string TypeName { get;}
string TypeDescription { get; }
IGH_Goo Duplicate();
new string ToString();
IGH_GooProxy EmitProxy();
bool CastFrom(object source);
bool CastTo<T>(out T target);
object ScriptVariable();
}
public interface GH_ISerializable
{
bool Write(GH_IWriter writer);
bool Read(GH_IReader reader);
}
可以看到,IGH_Goo
继承自 GH_Iserializable
,也就是说,一个类如果要实现 IGH_Goo
,上述所有的函数都必须实现。
看起来好多函数啊,不过仔细品一品也没有那么复杂。下面我们就使用一个例子,来自定义一个缝合怪类的数据,里面放一堆乱七八糟的数据,模拟各种使用的场景:
public class Pudge
{
public int PudgeInt { get; set; }
public List<double> PudgeDoubleList { get; set; }
public Dictionary<Guid, Curve> PudgeCurveDictionary { get; set; }
public Circle PudgeCircle { get; set; }
public TextEntity PudgeTextEntity { get; set; }
}
好的直接开始一个接口的实现:
看起来还挺多的,笔者这里直接附上完整实现的结果,并加上comments:
using GH_IO.Serialization;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
// ... namespace省略了
public class Pudge : IGH_Goo
{
// 重写一下 ToString() 方法,方便后续展示时看出区别
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Int: {PudgeInt}");
sb.AppendLine($"DList: {string.Join(", ", PudgeDoubleList)}");
sb.AppendLine($"CD: {{{string.Join("; ", PudgeCurveDictionary.Select(kvp => $" Key:{kvp.Key}, Curve:{kvp.Value}"))}}}");
sb.AppendLine($"Circle: {PudgeCircle}");
sb.AppendLine($"Entity: {PudgeTextEntity.PlainText}");
return sb.ToString();
}
// 原来的各种属性
public int PudgeInt { get; set; }
public List<double> PudgeDoubleList { get; set; }
public Dictionary<Guid, Curve> PudgeCurveDictionary { get; set; }
public Circle PudgeCircle { get; set; }
public TextEntity PudgeTextEntity { get; set; }
// 判断这个数据实例是否是一个合法正确的数据实例
// 举例场景:比如说这个数据里保存了一根直线,某些情况下这个直线可能长度为零了,这可能会造成一些奇奇怪怪的问题,
// 此时你可能就想要返回一个false
// 在这个例子里,因为我们存储了一个Circle和一些Curve,它们本身是自带了IsValid方法,我们就依照这些方法的返回结果来返回即可:
public bool IsValid
{
get
{
var all_curves_valid = false;
if (PudgeCurveDictionary == null || PudgeCurveDictionary.Values.Count == 0)
{
all_curves_valid = true;
}
else
{
all_curves_valid = PudgeCurveDictionary.Values.All(curve => curve.IsValid);
}
return all_curves_valid && PudgeCircle.IsValid;
}
}
// 如果上面的IsValid返回的是false,在这里可以返回一个字符串用来说明为什么
// 这里就简单写一个字符串举例,当然您可以提供更复杂的逻辑来说明具体哪个出问题了
public string IsValidWhyNot => "某一Curve或Circle数据验证失败";
// 这里返回一个字符串,用来说明这个数据类型的名字是什么,可以与类名本身不一样,这里就保持一致好了
public string TypeName => nameof(Pudge);
// 这里返回一个字符串,用来说明这个类是用来干什么的
public string TypeDescription => "一个数据缝合怪类型,用来做数据范例";
// 该函数是用来说明,如果其他的数据想要转换为这个类型的数据,转换将如何进行?
// source内就是这个“其他数据”
// 操作成功则返回true,操作失败就返回false
// 比如一个int类型、Curve类型、或者Circle类型的数据,想要转换成为这个Pudge类型,要怎么转换:
public bool CastFrom(object source)
{
var type = source.GetType();
if (type == typeof(int))
{
PudgeInt = (int)source;
return true;
}
else if (type == typeof(double))
{
if (PudgeDoubleList != null)
{
PudgeDoubleList.Add((double)source);
}
else
{
PudgeDoubleList = new List<double> { (double)source };
}
return true;
}
else if (type == typeof(Circle))
{
PudgeCircle = (Circle)source;
return true;
}
// 更多其他的情况这里就不写了,直接返回false来表明转换失败
else
{
return false;
}
}
// 该函数是用来说明,如果这个Pudge类型的数据想要转换为其他类型的数据,转换将如何进行?
// 操作成功则返回true,操作失败就返回false
public bool CastTo<T>(out T target)
{
if (typeof(T) == typeof(int))
{
target = (T)((object)PudgeInt);
return true;
}
// 其他的转换情况,本例就不写了,直接返回false来表明转换失败
else
{
target = default(T);
return false;
}
}
// 该函数是用来告诉Grasshopper实现复制操作时该如何操作本数据,函数返回结果为一个新的复制出来的Pudge类型的数据
public IGH_Goo Duplicate()
{
var copied_pudge = new Pudge();
copied_pudge.PudgeInt = PudgeInt;
copied_pudge.PudgeDoubleList = new List<double>(PudgeDoubleList);
copied_pudge.PudgeCircle = PudgeCircle; // Circle是struct类型,是ValueType,值传递,所以可以直接赋值,在内存中会存在两个不同的对象
copied_pudge.PudgeTextEntity = PudgeTextEntity.Duplicate() as TextEntity; // TextEntity是class类型,引用传递,所以必须进行一次拷贝构造,才能在内存中有两个对象,否则是同一对象的两个引用
copied_pudge.PudgeCurveDictionary = new Dictionary<Guid, Curve>(); // Curve类型是class,是引用传递,所以与TextEntity类似,每一个都需要拷贝构造
foreach (var kvpair in PudgeCurveDictionary)
{
copied_pudge.PudgeCurveDictionary.Add(kvpair.Key, kvpair.Value.DuplicateCurve());
}
return (IGH_Goo)copied_pudge;
}
// 如何创建这个类的“代理对象”。 关于“代理对象”是什么,可能大部分人应该不会接触到,以后有需求再讲吧。这里我们可以向Grasshopper声明本类型不支持代理对象,直接返回null即可
public IGH_GooProxy EmitProxy() => null;
// 当这个数据类型被传递至C# Script电池中的时候,它会有机会进行一个数据的转换,将它变成另一种数据格式(可能带来便利?)
// 直接返回它自身即可
public object ScriptVariable()
{
return this;
}
// 下面,就是我们熟悉的Read/Write函数,也就是执行序列化/反序列化时的函数了
// 实现了这两个函数,就能够实现我们所说的“在复制粘贴和文件保存打开时数据不丢失”的功能,也就是IGH_Goo最重要的意义
public bool Read(GH_IReader reader)
{
var succeeded = true;
PudgeInt = reader.GetInt32("pudge_int"); // 读取int
PudgeDoubleList = reader.GetDoubleArray("pudge_doubles").ToList(); //读取double
// 读取 Curve
if (PudgeCurveDictionary == null)
{
PudgeCurveDictionary = new Dictionary<Guid, Curve>();
}
else
{
PudgeCurveDictionary.Clear();
}
int total_curve_count = reader.GetInt32("pudge_courve_count");
for (int i = 0; i < total_curve_count; i++)
{
var guid = reader.GetGuid("pudge_curve_guids", i);
var chunk = reader.FindChunk("pudge_curves", i);
if (chunk == null)
{
succeeded = false;
continue;
}
GH_Curve gc = new GH_Curve();
gc.Read(chunk);
PudgeCurveDictionary.Add(guid, gc.Value);
}
// 读取 Circle
{
var chunk = reader.FindChunk("pudge_circle");
if (chunk == null)
{
succeeded = false;
}
else
{
var circle = new GH_Circle();
circle.Read(chunk);
PudgeCircle = circle.Value;
}
}
// 读取 TextEntity
var formatter = new BinaryFormatter();
var bytes = reader.GetByteArray("pudge_textEntity");
using (var ms = new MemoryStream(bytes))
{
PudgeTextEntity = (TextEntity)formatter.Deserialize(ms);
}
return succeeded;
}
public bool Write(GH_IWriter writer)
{
var succeeded = true;
// 保存 PudgeInt
writer.SetInt32("pudge_int", PudgeInt);
// 保存 List<double>
writer.SetDoubleArray("pudge_doubles", PudgeDoubleList.ToArray());
// 保存 curves
int curve_count = 0;
foreach (var item in PudgeCurveDictionary)
{
writer.SetGuid("pudge_curve_guids", curve_count, item.Key);
var gh_curve = new GH_Curve(item.Value);
var chunk = writer.CreateChunk("pudge_curves", curve_count++);
succeeded = gh_curve.Write(chunk) && succeeded;
}
writer.SetInt32("pudge_courve_count", curve_count);
// 保存 Circle
{
GH_Circle circle = new GH_Circle(PudgeCircle);
var chunk = writer.CreateChunk("pudge_circle");
succeeded = circle.Write(chunk) && succeeded;
}
// 保存 TextEntity
{
try
{
var formatter = new BinaryFormatter();
using (var ms = new MemoryStream())
{
formatter.Serialize(ms, PudgeTextEntity);
writer.SetByteArray("pudge_textEntity", ms.ToArray());
}
}
catch
{
succeeded = false;
}
}
return succeeded;
}
}
这样,我们在复制这个数据类的时候就不会出现问题了,文件打开和保存也不会存在任何问题:
public class Pudge // : IGH_Goo
{
// ... ...
}
重新编译,再尝试刚刚同样的操作:
可见,不实现 IGH_Goo
接口的数据类型,在直接进行复制粘贴、或者文件保存打开的操作的时候,的确会出现数据丢失的。
好了,本文作为上篇,介绍了为什么要存在 IGH_Goo
数据类型:【保证数据在GH画布上传递、操作时的数据完整性】。下篇将会介绍如何将我们的 IGH_Goo
类型与 Grasshopper 电池之间的数据处理逻辑的具体操作联系起来,🦀