Thanks for trying out Script Forge and providing the feedback, @dwydd_nu.
Header generation
On generating the header: Have you taken a look at the write-python-script and write-csharp-script skills in rhino-gh-kit? They are built for Claude, but ChatGPT might be able to port them over. They are set up precisely so that the LLM can handle the header information and formatting and you don’t have to think about it. Alternatively, you could directly provide the whole grammar to ChatGPT from header-reference.md.
Script Forge itself is not an AI-enabled tool; it can’t infer anything like an LLM which is why the specific header syntax is required. But you should be able to push ChatGPT further to write a header that contains all the necessary information in the right syntax.
Case sensitivity and unknown keys
Thank you for catching the “Tree” vs “tree” issue. In the latest release every key and every value in the header is matched without regard to case to allow more flexibility. But unknown keys now raise a warning to flag typos rather than being silently ignored.
Input and output names
Regarding the user-friendly “Name (for humans, optional)," this already exists as the name key. But you may want to pair it with variableName.
variableName is the identifier your code receives, and it’s also what Grasshopper draws as the param on the script component. It has to be a valid identifier.
name is the friendly human name. It shows up in the tooltip that pops up when you hover the param.
- If you leave
variableName out, it falls back to name, and in that case name has to be a valid identifier, too. That could be the substitution that caused the confusion, and I agree it’s a little surprising.
One thing to be aware of: on a forged script component the drawn label is always the variable name. That is Grasshopper’s behavior for script params and isn’t something the Forge chooses. So the friendly name lives in the tooltip only. If you compile the component with the kit’s other tools you can also set the short drawn label with nickname, but this key will do nothing for a forged script component.
Here is an example script based on your AI generated comment section. I used Claude to write it. Try plugging it into the Script Forge to see how the inputs and outputs are created on the script component.
smooth-values.py
"""@component
{
"name": "Smooth Values",
"nickname": "Smooth",
"description": "Iteratively smooths a per-point value field along each branch's ordered point sequence. Values may be numbers or vectors.",
"inputs": [
{ "name": "Points", "variableName": "points", "nickname": "P",
"type": "Point3d", "access": "tree",
"description": "One branch per chain, points in order. Either one branch per value branch with matching structure, or a single branch reused for every value branch." },
{ "name": "Values", "variableName": "value", "nickname": "V",
"type": "object", "access": "tree",
"description": "The field to smooth, one value per point. Numbers or Vector3d; left unhinted so both pass through unchanged." },
{ "name": "Boundary Fix", "variableName": "boundary_fix", "nickname": "B",
"type": "bool", "access": "item", "default": true,
"description": "Pin the first and last value of an open chain so the ends never drift. Ignored when Wrap is on." },
{ "name": "Wrap", "variableName": "wrap", "nickname": "W",
"type": "bool", "access": "item", "default": false,
"description": "Treat each branch as a closed loop, so the last point neighbours the first." },
{ "name": "Damping", "variableName": "damping", "nickname": "D",
"type": "double", "access": "item", "default": 0.5,
"description": "How far each value moves toward its neighbours per pass, 0 to 1. 0 changes nothing." },
{ "name": "Factor", "variableName": "factor", "nickname": "F",
"type": "double", "access": "item", "default": 1.0,
"description": "Extra scale on the smoothing step, applied on top of Damping." },
{ "name": "Max Iterations", "variableName": "max_iterations", "nickname": "I",
"type": "int", "access": "item", "default": 10,
"description": "How many smoothing passes to run." }
],
"outputs": [
{ "name": "Smoothed Values", "variableName": "value_out", "nickname": "V",
"type": "object", "access": "tree",
"description": "The smoothed field, on the same branch paths as Values." }
]
}
"""
import Rhino.Geometry as rg
from Grasshopper import DataTree
from Grasshopper.Kernel.Data import GH_Path
# --- defaults -----------------------------------------------------------------
# An unwired input arrives as None, so every read is guarded. The header's
# `default` seeds the param, but a user can still clear it by hand.
fix_ends = True if boundary_fix is None else boundary_fix
closed = False if wrap is None else wrap
damp = 0.5 if damping is None else damping
fac = 1.0 if factor is None else factor
passes = 10 if max_iterations is None else max_iterations
step = damp * fac
def neighbours(k, n):
"""Index pair either side of k, or None where the chain ends."""
if closed:
return (k - 1) % n, (k + 1) % n
return (k - 1 if k > 0 else None, k + 1 if k < n - 1 else None)
def smooth_branch(pts, vals):
"""One chain: move each value toward its distance-weighted neighbour average."""
n = len(vals)
if n < 3 or step == 0.0:
return list(vals)
current = list(vals)
for _ in range(max(passes, 0)):
nxt = list(current)
for k in range(n):
a, b = neighbours(k, n)
if a is None or b is None:
# An open chain's endpoint: pin it, or let its one neighbour pull it.
if fix_ends:
continue
a = a if a is not None else b
b = a
# Inverse-distance weights, so unevenly spaced points smooth evenly.
da = pts[k].DistanceTo(pts[a])
db = pts[k].DistanceTo(pts[b])
wa = 1.0 / da if da > 1e-12 else 0.0
wb = 1.0 / db if db > 1e-12 else 0.0
if wa + wb == 0.0:
continue
# `*` and `+` carry both a float and a Vector3d, which is the whole
# reason Values is left unhinted.
avg = (current[a] * wa + current[b] * wb) * (1.0 / (wa + wb))
nxt[k] = current[k] + (avg - current[k]) * step
current = nxt
return current
# --- walk the branches by hand ------------------------------------------------
# Tree access plus an explicit loop is what keeps output paths equal to input
# paths. Implicit iteration would append an index and break the pairing.
out = DataTree[object]()
if points is not None and value is not None:
broadcast = points.BranchCount == 1
for i in range(value.BranchCount):
path = value.Path(i)
vals = list(value.Branch(i))
j = 0 if broadcast else i
pts = list(points.Branch(j)) if j < points.BranchCount else []
if len(pts) != len(vals):
# Structure mismatch: pass the branch through untouched rather than
# emitting a shorter branch and silently shifting everything after it.
out.AddRange(vals, path)
continue
out.AddRange(smooth_branch(pts, vals), path)
value_out = out