Hi everyone!
Does anyone know if it’s possible to automatically save a list of integers to persistent memory (bake an integer data tree) in gh?
I can manually click “internalize data.” But I want to make it so that if no data is currently being fed to the integer node (or another node or script), the integer node will pass on the old values. But if new values are fed to the integer node, it should remember them and pass them on, even if the integer input is then an empty list again.
I understand this can’t be done with a C# script, since the script will store the information in RAM, and when gh and rhino are saved and reopened, all the old data in the script’s output is erased. For now, the only option is a script that saves all the numbers to files in the same folder as the Grasshopper script. But that would create too many files. Is there really no way to implement this functionality within Grasshopper?
I tried writing a C# script, but it only works until I exit Rhino and Grasshopper. After they restart, the script forgets all the old source data because it can’t store information in persistent memory.
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
public class Script_Instance : GH_ScriptInstance
{
#region Notes
#endregion
private DataTree<int> \_cachedData = new DataTree<int>();
private void RunScript(DataTree<int> x, ref object i)
{
bool hasNewData = (x != null && x.DataCount > 0);
if (hasNewData)
{
if (!AreTreesEqual(x, \_cachedData))
{
\_cachedData = new DataTree<int>(x);
}
}
else
{
}
i = \_cachedData;
}
private bool AreTreesEqual(DataTree<int> treeA, DataTree<int> treeB)
{
if (treeA == null && treeB == null) return true;
if (treeA == null || treeB == null) return false;
if (treeA.DataCount != treeB.DataCount) return false;
if (treeA.BranchCount != treeB.BranchCount) return false;
var pathsA = treeA.Paths;
var pathsB = treeB.Paths;
for (int k = 0; k < pathsA.Count; k++)
{
if (pathsA\[k\].CompareTo(pathsB\[k\]) != 0) return false;
var listA = treeA.Branch(k);
var listB = treeB.Branch(k);
if (listA.Count != listB.Count) return false;
for (int j = 0; j < listA.Count; j++)
{
if (listA\[j\] != listB\[j\]) return false;
}
}
return true;
}
Thanks, but you still need some login information. And the script chooses between the two specified data. I need the program to remember a list of numbers and automatically reproduce it even if that list no longer exists. But when a new list of numbers is submitted, the program should forget the old list and remember the new one, and so on.
Maybe. How exactly would I use this? I could probably create a C# script that would save the lists of numbers to a separate file and check it each time. But the problem is that I’d have to use separate files for many cases within a single GH script. That would probably result in a lot of external files. So I was hoping to be able to do Automatically Internalize Data within the GH script.
It’s kind of like a Geometry Pipeline - you have a DataOutput - plug your data in there, right click the component and set a Destination file - this file will be synced with the last set of data in the component (so maybe a DataDam to control when to refresh) - the DataInput is similar, but it reads data from the file you made
You can load DataTrees into DataOutput, so I don’t think you need to create lots of files
I guess the other thing is just to make a CSV the old fashioned way?
HERE a simple example (Have not tried that in a c# component though)
using Grasshopper.Kernel;
using GH_IO.Serialization;
using System;
public class SimpleStringComponent : GH_Component
{
private string _savedText = "default";
......
// Save the string into the .gh file
public override bool Write(GH_IWriter writer)
{
if (!string.IsNullOrEmpty(_savedText))
writer.SetString("SimpleText", _savedText);
return base.Write(writer);
}
// Load the string from the .gh file
public override bool Read(GH_IReader reader)
{
if (reader.ItemExists("SimpleText"))
{
_savedText = reader.GetString("SimpleText");
}
else
{
_savedText = "default"; // fallback if missing
}
return base.Read(reader);
}
}
Thank you! These methods don’t work in a C# script inside Grasshopper. But I used these methods and Google Gemini to create a .gha plugin in Visual Studio and compile it. Now this plugin works perfectly within the .gh script without creating any third-party files
I used this code to create a .gha plugin for Grasshopper using Visual Studio:
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using Rhino.Geometry;
namespace MyPersistentPlugin
{
public class PersistentIntegerComponent : GH_Component
{
// — ВНУТРЕННЯЯ ПАМЯТЬ —
private DataTree _cachedData = new DataTree();
/// <summary>
/// Конструктор компонента. Настраиваем имя, никнейм, описание и категорию.
/// </summary>
public PersistentIntegerComponent()
: base("Persistent Integer", "PInt",
"Запоминает дерево целых чисел и сохраняет его внутри .gh файла",
"Params", "Util")
{
}
/// <summary>
/// Регистрация входных параметров.
/// </summary>
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
// Вход 0: x, Integer, Tree Access
pManager.AddIntegerParameter("Data", "x", "Входящее дерево целых чисел", GH_ParamAccess.tree);
// Делаем параметр опциональным, чтобы компонент не ругался при пустом входе
pManager[0].Optional = true;
}
/// <summary>
/// Регистрация выходных параметров.
/// </summary>
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
// Выход 0: i
pManager.AddIntegerParameter("Stored", "i", "Сохраненные или новые данные", GH_ParamAccess.tree);
}
/// <summary>
/// Основная логика (выполняется при пересчете).
/// </summary>
protected override void SolveInstance(IGH_DataAccess DA)
{
// 1. Получаем входное дерево
if (!DA.GetDataTree(0, out GH_Structure<GH_Integer> inputStructure)) return;
// Преобразуем GH_Structure в удобный DataTree<int> для работы
DataTree<int> inputTree = StructureToTree(inputStructure);
// 2. Логика сравнения
bool hasNewData = inputTree.DataCount > 0;
if (hasNewData)
{
// Если данные отличаются от сохраненных -> обновляем память
if (!AreTreesEqual(inputTree, _cachedData))
{
_cachedData = new DataTree<int>(inputTree);
}
}
// 3. Вывод данных (всегда выводим то, что в памяти)
// Преобразуем обратно в GH_Structure для вывода
DA.SetDataTree(0, TreeToStructure(_cachedData));
}
// --- СОХРАНЕНИЕ И ЗАГРУЗКА (САМОЕ ГЛАВНОЕ) ---
/// <summary>
/// Вызывается при сохранении файла .gh
/// </summary>
public override bool Write(GH_IO.Serialization.GH_IWriter writer)
{
// Превращаем дерево в строку
string serializedData = SerializeTreeToString(_cachedData);
// Записываем строку в файл под ключом "SavedTree"
writer.SetString("SavedTree", serializedData);
return base.Write(writer);
}
/// <summary>
/// Вызывается при открытии файла .gh
/// </summary>
public override bool Read(GH_IO.Serialization.GH_IReader reader)
{
// Проверяем, есть ли наш ключ в файле
if (reader.ItemExists("SavedTree"))
{
string dataString = reader.GetString("SavedTree");
// Восстанавливаем дерево
_cachedData = DeserializeStringToTree(dataString);
}
else
{
_cachedData = new DataTree<int>();
}
return base.Read(reader);
}
// --- ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ---
private bool AreTreesEqual(DataTree<int> treeA, DataTree<int> treeB)
{
if (treeA.DataCount != treeB.DataCount) return false;
if (treeA.BranchCount != treeB.BranchCount) return false;
for (int i = 0; i < treeA.BranchCount; i++)
{
if (treeA.Paths[i].CompareTo(treeB.Paths[i]) != 0) return false;
var listA = treeA.Branch(i);
var listB = treeB.Branch(i);
if (listA.Count != listB.Count) return false;
for (int j = 0; j < listA.Count; j++)
if (listA[j] != listB[j]) return false;
}
return true;
}
// Конвертер: Grasshopper Structure -> C# DataTree
private DataTree<int> StructureToTree(GH_Structure<GH_Integer> structure)
{
DataTree<int> tree = new DataTree<int>();
if (structure == null) return tree;
foreach (GH_Path path in structure.Paths)
{
var branch = structure.get_Branch(path);
foreach (GH_Integer val in branch)
{
if (val != null) tree.Add(val.Value, path);
}
}
return tree;
}
// Конвертер: C# DataTree -> Grasshopper Structure
private GH_Structure<GH_Integer> TreeToStructure(DataTree<int> tree)
{
GH_Structure<GH_Integer> structure = new GH_Structure<GH_Integer>();
for (int i = 0; i < tree.BranchCount; i++)
{
var path = tree.Paths[i];
var list = tree.Branch(i);
foreach (int val in list)
{
structure.Append(new GH_Integer(val), path);
}
}
return structure;
}
// Сериализация: Дерево -> Строка "0;0:1,2|0;1:3"
private string SerializeTreeToString(DataTree<int> tree)
{
if (tree.DataCount == 0) return "empty";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tree.BranchCount; i++)
{
string pathStr = tree.Paths[i].ToString().Replace("{", "").Replace("}", "").Trim();
sb.Append(pathStr + ":");
sb.Append(string.Join(",", tree.Branch(i)));
if (i < tree.BranchCount - 1) sb.Append("|");
}
return sb.ToString();
}
// Десериализация: Строка -> Дерево
private DataTree<int> DeserializeStringToTree(string s)
{
DataTree<int> tree = new DataTree<int>();
if (string.IsNullOrEmpty(s) || s == "empty") return tree;
try
{
string[] branches = s.Split('|');
foreach (var b in branches)
{
string[] parts = b.Split(':');
if (parts.Length != 2) continue;
string[] idxStrs = parts[0].Split(';');
List<int> idxs = new List<int>();
foreach (var x in idxStrs) if (int.TryParse(x, out int k)) idxs.Add(k);
GH_Path path = new GH_Path(idxs.ToArray());
string[] numStrs = parts[1].Split(',');
foreach (var n in numStrs) if (int.TryParse(n, out int v)) tree.Add(v, path);
}
}
catch { }
return tree;
}
/// <summary>
/// Уникальный GUID компонента. ВАЖНО: Не меняйте его после релиза.
/// </summary>
public override Guid ComponentGuid
{
get { return new Guid("12345678-ABCD-EF00-1234-56789ABCDEF0"); }
// Совет: Сгенерируйте свой GUID в VS: Tools -> Create GUID
}
/// <summary>
/// Иконка компонента (null = стандартная иконка)
/// </summary>
protected override System.Drawing.Bitmap Icon => null;
}