generative modeling for Rhino
Dear GHA developers,
this is an important message for those of you who have components that output lists of data using DA.SetDataList().
It is important that you make sure you always call SetDataList() from within SolveInstance(), even if you do not have any data to assign. There is some logic in place that will automatically create output data structures, but this logic may result in unexpected and unpredictable (from the user point of view) data when you omit a call to SetDataList(). A typical implementation of SolveInstance() should look something like:
Dim A As SomeData = Nothing
Dim B As SomeData = Nothing
If (A Is Nothing) OrElse (B Is Nothing) OrElse _
(Not A.IsValid) OrElse (Not B.IsValid) Then
'Do more stuff here, but always call
'SetDataList() before any Return statements.
or, if you're cool:
SomeData A = null;
SomeData B = null;
DA.GetData(0, ref A);
DA.GetData(1, ref B);
if( A == null | B == null | !A.IsValid | !B.IsValid )
//Do more stuff here, but always call
//SetDataList() before any return statements.
I'll add a help topic to the SDK documentation explaining this in more detail as well. The logic that invents a reasonable data structure layout for output parameters is being beefed up for 0.9+ and there will be changes to the way it behaves but I realised I cannot actually fix all possible quirks unless SetDataList() is consistently called.
the lowdown is such: Grasshopper will do its very best to fill in the gaps when you bail. However 'filling in the gap' means something different for parameters which are outputting lists than those that are outputting single items. In the case of single items, I need to add a NULL to make sure the data on the right matches the data on the left index by index. In the case of lists I need to add an empty list.
I can do the right thing almost always, except when it's the first time your component runs. Then I simply don't know whether you were about to add a list or an item before you aborted and I cannot therefore do the correct thing all the time. I default to adding a single NULL rather than an empty list, because that seems to be the more common case, but it will be wrong in non-item cases.
The proper solution would be to demand an access level when registering output parameters, and then disallowing other SetDataXXXX methods, just like I currently do on input parameters, but that would be a massive SDK break.
How about adding something to tell you in advance from the code what iteration it would be, like
DA.RegisterOutputAs(ListAccess, i) ?
Then one could call this once in the beginning and just be sure it will always work. Otherwise, default to item (current behavior). Just a thought.
Then I'd rather add overloads in RegisterOutputParameters so you can specify ahead of time what sort of data you'll be outputting. That's probably a good idea as it will make writing SolveInstance implementations easier, but this actually is an SDK change (though not a break).
Regarding your code, it's typically a bad idea to use both SetData and SetDataList depending on local variables. I do this myself in a few cases, but I regret most of them already.
You don't have to do SetData(0, null); because that happens by default, what you should do is SetDataList(0, null) to ensure an empty list is added always.
In the rare case where you don't even want an empty list, just completely nothing, then you can always choose to disable the filling-in-the-gap logic using DA.DisableGapLogic(0)
Thanks for the clarification David, this makes a lot of sense.
While we're on the topic of behaviour of data structures at output parametres, may I request something:
The default behaviour of Grasshopper is to only add more paths to an existing tree as new operations are performed on it, but not remove paths. For instance:
if I reference a bunch of curves from Rhino, and extract their CVs, I get a tree of points, 1 list per curve. If I take these CVs and put them back into creating NurbsCurves, I will end up with a tree of Curves, but only 1 curve per list, as the behaviour is not set to remove the last path when lists get combined to form singular entities.
I find that its often counter-intuitive that paths are added on an operation that's 1-object-in-many-objects-out, but not removed when the operation is many-objects-in-1-object-out.
It essentially gives you out a grafted list, which often requires a pathMapper to get rid of that last index in the structure. However, if default behaviour was indeed to remove the last index in the structure and the user wanted it back, its a one-click operation to graft the list again -- Just a suggestion.