Every Grasshopper component is the same process. Subclass GH_Component. Hand-register each input and output. Invent a stable GUID. Draw a 24×24 icon. Marshal types in and out of SolveInstance. That's forty lines of plumbing before you write the one line you actually came for.
Write the one line. Betta writes the forty. Decorate a plain C# method with an attribute — Betta reflects over it at load, builds the ports, assigns a deterministic GUID, picks an icon, and drops a real component on the canvas. It's the idea behind Dynamo's ZeroTouch, brought to Grasshopper: put a DLL in, get nodes out.
[GrasshopperCollection("Strings", "Text")]
public interface IStringCollection : IBettaCollection
{
[GrasshopperMethod("Upper")]
string ToUpper([GrasshopperParameter("Text")] string text);
[GrasshopperMethod("Concat")]
string Concat(string left, string right, string separator);
}That's a ribbon tab, two components, and all their ports — done. No GH_Component, no RegisterInputParams, no SolveInstance, no GUID to babysit.
👉 The same single component, hand-written the usual way
public class CubeComponent : GH_Component
{
public CubeComponent() : base("Cube", "Cube", "x³", "Quickstart", "Demo") { }
protected override void RegisterInputParams(GH_InputParamManager pm)
=> pm.AddNumberParameter("Value", "V", "number to cube", GH_ParamAccess.item, 2.0);
protected override void RegisterOutputParams(GH_OutputParamManager pm)
=> pm.AddNumberParameter("Result", "R", "x³", GH_ParamAccess.item);
protected override void SolveInstance(IGH_DataAccess DA)
{
double x = 0.0;
if (!DA.GetData(0, ref x)) return;
DA.SetData(0, x * x * x); // ← the only line you meant to write
}
public override Guid ComponentGuid => new Guid("d3b07384-…-0001");
protected override Bitmap Icon => null;
}With Betta, the method body is the component. Everything wrapped around it above is what Betta generates for you.
Your services stay plain and framework-agnostic — no Grasshopper types leak in, so the same code is unit-testable with no Rhino in sight. Betta is just the thin shell that puts it on the canvas.
And every component wears the same betta silhouette, so the family reads as one on the canvas — while each library keeps its own character: its naming, its ports, its geometry. Consistent enough to feel native, distinct enough to feel like itself.
Betta can be used for AI-assisted authoring — the repo ships a Claude Code skill and a CLAUDE.md that hand a coding agent the contract: the attributes, the return-type → output mapping, the deploy path, the GUID-stability gotchas. Drop any agent session into this repo and it knows how to add a component without being told twice.
Sure, an LLM can author a full GH_Component from scratch — and it usually does, after a few rounds of fixing the GUID, the param-access modes, and the icon plumbing. With Betta the agent only writes the function body; everything around it is generated deterministically at runtime, so refactors don't re-spend tokens on boilerplate and there's far less hallucinated code in the diff to review.
- Build
Betta.sln -c Release(VS 2022 or CLI). The post-build copiesBetta.gha+ deps into%AppData%\Grasshopper\Libraries\. - Launch Rhino 8 → Grasshopper — the Betta and sample Strings tabs appear on the ribbon.
Or grab the latest release (drop-in .zip or Rhino Package Manager .yak). Components missing? The startup log lists everything published at %AppData%\Grasshopper\Libraries\Betta.log.
Requires Rhino 8 on Windows. Plugins target net7.0-windows (or any TFM that can reference netstandard2.0).
- New class library (
net7.0-windows). - Reference
Betta.Abstractions— the SDK contract, not the.gha— withExcludeAssets="runtime". - Write a collection (class-direct or interface + impl); both opt in by inheriting
IBettaCollection. - Build, drop the DLL into
%AppData%\Grasshopper\Libraries\Betta\. Betta watches the folder and hot-adds it — no restart.
<PackageReference Include="Betta.Abstractions" Version="0.3.1" ExcludeAssets="runtime" />[GrasshopperCollection(Category = "MyPack", SubCategory = "Maths")]
public class MyService : IBettaCollection
{
[GrasshopperMethod("Cube")]
public double Cube([GrasshopperParameter("Value")] double x) => x * x * x;
}If a class implements a marked interface, the interface wins (no double registration).
| Attribute | Target | Purpose |
|---|---|---|
[GrasshopperCollection(category, sub)] |
interface / class | Default Category + SubCategory for the type's methods. |
[GrasshopperMethod(name)] |
method | Publishes the method as a component. Also: NickName, Description, Category, SubCategory, IconResource, Guid, Enabled. |
[GrasshopperParameter(name, …)] |
parameter | Input display name/nickname/description. List<T> → list input automatically. DefaultValue = … seeds unwired sockets. |
| Return type | Outputs |
|---|---|
| primitive / Rhino geometry | one output |
List<T> |
one list output |
ValueTuple |
one per element (named tuple → named outputs) |
| custom class | one per public property |
Task<T> / ValueTask<T> |
async — cached by input hash, re-solves on completion |
Name outputs explicitly with [return: GrasshopperOutput("Result", "R", "…")] (repeat with Index per tuple element).
Each component wears one of six embedded Mini betta silhouettes — Amber, Aqua, Cosmic, Forest, Lime, Rose — picked deterministically from its GUID and rendered at 24×24 (content-cropped, aspect-preserving, no recoloring):
var idx = descriptor.Guid.ToByteArray()[0] % SessionFish.Count;Same component → same fish, every session and every machine. Prefer your own art? [GrasshopperMethod("Foo", IconResource = "my_icon.png")] ships an embedded PNG, rendered verbatim.
IBettaCollection interface/class [Betta.Abstractions]
↓ reflection at startup
ComponentRegistry → ComponentDescriptor [Betta.gha] one per [GrasshopperMethod]
↓
BettaComponentProxy (IGH_ObjectProxy) Instances.ComponentServer.AddProxy
↓ on canvas drop
BettaComponent (GH_Component) generic; all proxies share one CLR type
↓ per solve
ParamInjector inputs → Method.Invoke(service, args) → outputs
Component GUIDs are deterministic — an MD5 of the method signature — so saved .gh files survive rebuilds and moving code between machines. Services resolve from a DI container built once in Startup.PriorityLoad, with logging injected automatically. Rename the display name freely; rename the method, type, or a parameter and the GUID changes (pin Guid = "…" to refactor without breaking saved files).
Betta.Abstractions/ public contract (netstandard2.0, NuGet-ready)
Betta/ runtime plugin (.gha) — Startup, Services, Components, Rendering
Betta.Strings/ sample plugin (project reference)
samples/
Betta.Quickstart/ minimal sample (NuGet package reference)
Betta.Tour/ guided tour: Basics → Intermediate → Advanced
TestBetta/ xUnit tests (Rhino.Inside, headless GH)
dotnet build Betta.sln -c Debug # plugin + samples
dotnet test TestBetta/TestBetta.csproj # needs Rhino 8 (Rhino.Inside)
dotnet pack Betta.Abstractions/Betta.Abstractions.csproj -c Release # → ./artifacts/*.nupkgBetta.Abstractions is the only NuGet-published project (GeneratePackageOnBuild is off — pack explicitly). samples/Betta.Quickstart restores it from the local artifacts/ feed to exercise the packaged authoring flow.
Tree inputs (GH_Structure<T>), hot-reload of replaced plugins, a canvas LoadLibrary component, and a real settings panel — see ROADMAP.md.
MPL-2.0. Plugins built on top are unaffected — the copyleft is file-level, attaching only to source files that carry Betta's MPL header (everything in Betta/ and Betta.Abstractions/). A plugin DLL that references Betta.Abstractions and runs against Betta.gha is its own work in its own files; ship it under any license you want — MIT, proprietary, paid, closed. Referencing, inheriting from, or extending Betta's types does not transfer MPL to your code.
The only ask: if you modify a Betta source file and distribute the modified version, you publish those changes back under MPL. Your own files stay yours.
