algorithmic modeling for Rhino
I'm trying to write a set of custom components, in C#, that will allow someone to control grasshopper through a MIDI controler. For example I have a Korg Nano Kontrol:
I was able to get a prototype of controlling Grasshopper with MIDI working earlier, but honestly what I had setup was kind of a hack. So now I'm trying to create a set of true custom Midi Controller components that would be easy for someone to reuse.
However, I'm stuck trying to figure out how I can trigger the solution to recalculate. I'm able to get values to change when I rotate a knob, but the changes aren't apparent in Grasshopper or the model until I manually click the rebuild button.
GH_Document.NewSolution() or GH_Document.ScheduleSolution() appear to be what I need to call, but I'm also not sure how I can even get a reference to the current GH_Document?
Tags:
Hi Eric,
if you have a GH_Component-deriving class you can either call ExpireSolution(true) which will rebuild the depending components and params, or use PingDocument() and, if the result is non-null, you can call the NewSolution(true) method.
You can have a look at the trampoline script source code as well, both in C# and Vb.Net.
The logic behind the two registrations to events ( += ) is due to the fact that when the current solution is finished (first event) then the timer is registered and then is triggers (second event). The lapse in the timer allows to have a response from the Gh interface, for example when changing the toggle.
- Giulio
________________
giulio@mcneel.com
McNeel Europe, Barcelona
I had tried calling ExpireSolution(true) and I was able to get it to update when I turned the knob on my controller but after a second or two a window will pop up with the following error
An exception was thrown inside the drawing pipeline:
Source: System.Drawing
Type: Object is currently in use elsewhere.
The display of objects will be disabled to prevent
this from happening again. Please save the file under a
different name and close Rhino.
We'd apreciate if you send the file in question to david@mcneel.com
After that the Canvas goes red and I have to restart Grasshopper.
I emailed David about this a day ago. Sounds like some kind of threading issue to me? I tried modifying my code so that it would track the last time it called ExpireSolution() and would check to see how long its been each time so that it would never call it withing some time span. Although, even at 1000 ms I was still getting the "Object is currently in use elsewhere" error.
One issue with this controller is it can generate a ton of events very quickly. Turning a knob from the left to the right will generate an event for each new value the knob passes from 0 to 128.
What *should* happen if a lot of components start calling ExpireSolution(true) repeatedly? Does Grasshopper know to ignore multiple requests until the current solution finishes? I can't look at the trampoline example right now because I'm at home, but it sounds like hooking up to the Solution Fininshed event and not requesting a new solution until the current solution as finished might solve my problem? I'll take a look at it tomorrow when I'm back in the office.
Hi Eric,
there's multiple issues here, I'll address them in separate posts.
1) ExpireSolution(True) vs. ExpireSolution(False) vs. NewSolution(True) vs. NewSolution(False)
Which method(s) you end up calling depends a lot on how often you expect to get these events and whether or not you want to update a single or multiple components at the same time. This is what the methods do respectively:
ExpireSolution(True): It sets the 'expired' flag on the component you call this method on and all objects that depend on that component. Basically everything downstream of it. When it's done setting all these 'expired' flags, it will place a call to GH_Document.NewSolution(False).
ExpireSolution(False): Does the same as the previous one, except it doesn't tell the document to re-solve itself. Use this method if you want to expire a bunch of different components before attempting a new solution.
NewSolution(True): This will set the 'expired' flags of all components inside the document and resolves everything. This is carpet bombing, rarely should you use this method.
NewSolution(False): This will only resolve those objects that have their 'expired' flags set.
So, to recap, given the following cases you should be using the associated approach:
- A single object was changed and we want to immediately resolve (for example when a user drags a number slider, or connects a wire). Use ExpireSolution(True).
- A bunch of objects are expired simultaneously (or nearly simultaneously) by some sort of massive off-site event. For example if you hook up multiple hardware controls or sensors and they're all firing updates all the time. Use ExpireSolution(False) on all the affected objects, then place a single call to NewSolution(False).
- Something massive has happened and you have no idea of what exactly it was. Use NewSolution(True), as you don't have enough information to limit the devastation of cached data.
Grasshopper is not smart enough to post-pone solutions briefly until it decided that no more calls to ExpireObject() are coming in. If you tell it to recompute, it will immediately recompute. If you need delayed smarts, you'll need to write those yourself.
--
David Rutten
david@mcneel.com
Poprad, Slovakia
Schedules a new solution sometime in the foreseeable future. You can specify how long Grasshopper should wait before starting the next solution. This is what the Timer object calls.
You can also supply a call-back so you'll get informed just before the new solution kicks off (so you can gather additional data or expire certain objects or whatever).
--
David Rutten
david@mcneel.com
Poprad, Slovakia
2) Threaded application vs. Non-Threaded applications.
Grasshopper is a non-threaded application and by definition, the interface portion of any winforms app is single-threaded. This poses problems when you get external events that occur on a different thread, as might well be the case for hardware controls or sensor data.
Here's essentially what happens:
Something expires (part of) a Grasshopper document, and the NewSolution() method is called upon to rectify this. It starts by iterating over all the objects inside the document and whenever it encounters an object which has the 'expired' flag set to true, it will tell it to recompute itself. This object cannot recompute itself unless all the objects it depends on are also 'solved', so it might cause a shock-wave of recomputes across the entire document. Eventually though all the inputs are available and it recomputes itself and then hands control back to the document solver.
The document solver now continues on its quest to find more 'expired' objects. It might find some in which case the above story is repeated, or maybe there was only ever one expired object, or maybe the object we just found caused all other expires objects to solve themselves, leaving nothing to do for the document solver.
When the document solver has iterated over all the objects and determined that none of them are still 'expired', it will place a call to redraw the Grasshopper canvas and the Rhino viewports.
That is what happens during a proper, single-threaded solution. When there's two (or more) threads operating on the same document, all hell breaks loose:
Let's assume the document solver is halfway done with its job of resolving all expired objects. At this point an external event comes in on a different thread, which is handled immediately by some component. This component places a call to ExpireSolution(True), which sets the 'expired' flags of a bunch of components. Some of these components will already have been handled by the solver in the first thread, so even though the solver thinks all is fine and dandy, these components have in fact expired again. Other components might still be expired because the solver hasn't gotten to them yet. Worst case scenario, a component just finished solving itself on the first thread and the data inside of it is being harvested by a downstream component. While it's collecting this data the second thread wipes the data, causing a mismatch and probably a crash.
The Grasshopper core algorithms are not thread-safe and if you start calling methods from different threads, you will run into clashes.
What you need to do is 'invoke' the first thread from your second thread. That way the first thread is allowed to finish what it's doing before getting around to your request. Invoking is a very important concept when you're writing a multi-threaded app with a GUI and there's plenty of online resources explaining how it works. Here's two:
http://weblogs.asp.net/justin_rogers/pages/126345.aspx
http://www.yoda.arachsys.com/csharp/threads/winforms.shtml
--
David Rutten
david@mcneel.com
Poprad, Slovakia
As usual David, thanks for the easy to understand and comprehensive response. I've dabbled in multithreading with a couple of my programs. So Invoke and BeginInvoke are not entirely new to me, but I'm far from an expert.
Anyway I was able to get a really rough version of my custom MIDI components working with your help. See the video below:
Cool! So did you end up invoking any methods? What winforms object did you invoke them on?
Also, what happens when your events are coming in quicker than Grasshopper can handle them. Do they just stack up forever or are you dropping events if you feel you're getting too far ahead?
--
David Rutten
david@mcneel.com
Poprad, Slovakia
Yes I did end up invoking methods as you suggested. When midi events occur on the working thread I invoke a SetValue() method on my components to update their value, and then I invoke the ExpireSolution(false) on the component to mark it as needing to be re-solved. At this point my code either starts or restarts a timer, which when complete invokes NewSolution(false).
My idea with the timer was that if the user moved the knob very quickly, I would want Grasshopper to try to solve on the value from the last knob position not the first new position it saw.
I used a reference to the ActiveCanvas through the GH_InstanceServer to call Invoke on. For example here's the code I used to expire a component.
Grasshopper.GH_InstanceServer.ActiveCanvas.Invoke(new Action/span>bool>(currentGHComponent.ExpireSolution), new object[] { false });
I just got this working right before I left the office tonight, so I didn't have a lot of time to test it. As you can see from the video I was testing a solution that can be solved almost instantly. On Monday I plan to test it with a solution that takes longer to solve. I'm curious what happens if I keep turning the knob on the controller while the solver is already running. I'm thinking the additional invokes to NewSolution() will stack up in the GUI message queue and end up running immediately after the current solution finishes one after the other. I'm not sure though. If that's the case I'll probably need to track if a NewSolution() invoke has already been sent out, and whether it has completed (SolutionEnd event fires) before making additional calls to NewSolution().
I have the project up on a SVN server if your interested in checking out the code.
Hey Eric, could I see this code?
Hi Eric, I'd love to see this code as well. I'm not sure if my plugin project needs the invoke methods yet, but if I can't solve my crashing problems I'm willing to try. Would love a sample in the GH context to help guide me.
Also curious to hear how your tests (on longer solves and continuous updates) turned out!
Thanks!
Marc
I actually somewhat got a midi controller input to work, but it is specific to my controller. If you would like we could team up and put out a quality midi plugin that is more universal.
I was hoping to put out an open source midi plugin so people could add presets for their own controllers and eventually we would have most of the mainstream ones covered
Welcome to
Grasshopper
Added by Parametric House 0 Comments 0 Likes
Added by Parametric House 0 Comments 0 Likes
Added by Parametric House 0 Comments 0 Likes
Added by Parametric House 0 Comments 0 Likes
© 2024 Created by Scott Davidson. Powered by