Hi there,
I’m trying to implement a continuous MouseCallback in Grasshopper. The goal is to have a GH component that continuously registers mouse clicks in Rhino viewports. (In detail: I want to retrieve a 3d point on mouse down and up, as if to drag smth from one place to another, to use it later similar to the Grab component in Kangaroo2 ).
I have a working Python Script version:
import System
import scriptcontext
import rhinoscriptsyntax as rs
import Rhino
def get_view_pt():
screenPos = System.Windows.Forms.Cursor.Position
doc = Rhino.RhinoDoc.ActiveDoc
view = doc.Views.ActiveView
point = view.ActiveViewport.ScreenToClient(screenPos)
line = view.ActiveViewport.ClientToWorld(point)
cplane = view.ActiveViewport.ConstructionPlane()
_, t = Rhino.Geometry.Intersect.Intersection.LinePlane(line, cplane)
P = line.PointAt(t)
return P
class MyMouseCallback(Rhino.UI.MouseCallback):
def __init__(self):
self.pt_mouse_down = None
self.pt_mouse_up = None
def OnMouseDown(self, args):
p = get_view_pt()
print "mouse down point : ",p
self.pt_mouse_down = p
self.pt_mouse_up = None
def OnMouseUp(self, args):
p = get_view_pt()
print "mouse up point : ",p
self.pt_mouse_up = p
self.add_drag_line
self.pt_mouse_down = None
self.pt_mouse_up = None
@property
def add_drag_line(self):
if self.pt_mouse_down and self.pt_mouse_up:
return rs.AddLine(self.pt_mouse_down, self.pt_mouse_up)
else:
pass
cb = MyMouseCallback()
cb.Enabled = True
while True:
Rhino.RhinoApp.Wait()
if scriptcontext.escape_test(False):
print "ESC pressed "
break
cb.Enabled = False
adapted from this post and this post.
How can I make it run in a Grasshopper component?
Hi, it’s always tricky to do these things in a GH script component. I wouldn’t do this. That’s the issue. You can try the following, which should work on a scripting level:
You need to make sure to register and instanciate it only once. Design it as a singleton class and put the instance in Rhinos global “sticky” dictionary. This object has to run on another thread. Make sure whenever you do something with core functionality, that you make it thread safe and invoke this change on the GUI thread.
Since Iron Python targets the net framework, check the Task library and google how you invoke something in the app main thread.( Dispatcher.Invoke… )
You might need to deal with the Grasshopper way of execution and when and where to invoke something. But as I said, it sounds complicated. Don’t forget that Kangaroo is a GH addin and has much more freedom in doing things apart from the GH logic.
I’ll have a look later on. Doesn’t look right. The singleton pattern I propose was to make sure you register everything just once. But you can of course just check if the sticky dictionary key was already set or not (and if not init). Singleton implementations are not fitting well to Python syntax since you cannot hide the constructor…
What I really don’t like is the Mouse callback class this is a bit odd (but maybe a C++ thing). I simply would expect an function pointer (delegate) or event to hook at and than just attach your logic to the mouse logic of Rhino. I will play around. If this is not going to work, there is always the low-level solution of using the winapi. Let’s see…
Sorry, haven’t had enough time. As I said scripting it together in Python is not a good solution here. Since this is a plugin, it probally handles this it much better anyway.
Even though it’s indeed definitelly cleaner to do this as a proper C# component, the Python Script solution isn’t that convoluted either.
import Rhino
import Rhino.Geometry as rg
import rhinoscriptsyntax as rs
import scriptcontext
from scriptcontext import sticky
scriptcontext.doc = Rhino.RhinoDoc.ActiveDoc
class CtrlDoubleClick(Rhino.UI.MouseCallback):
"""Our custom MouseCallback class"""
def xy_target(self, e):
"""Get target point in WordlXY plane based on cursor location."""
line = e.View.ActiveViewport.ClientToWorld(e.ViewportPoint)
param = rg.Intersect.Intersection.LinePlane(line, rg.Plane.WorldXY)[1]
pt = line.PointAt(param)
return pt
def OnMouseDoubleClick(self, e):
"""Override for DoubleClick event, which adds point if CTRL is pressed."""
if e.CtrlKeyDown:
pt = self.xy_target(e)
rs.AddPoint(pt)
# We use sticky dict to keep track of the created callbacks
# and make sure that we always have only one active.
# Ideally we should also dispose of the old ones.
def create_callback(clss, update=False):
# init on first load
if clss.__name__ not in sticky:
sticky[clss.__name__] = clss()
# if update disable last instance and create a new one
if update:
sticky[clss.__name__].Enabled = False
sticky[clss.__name__] = clss()
def enable_callback(clss, enabled=True):
# an user input driven enable/disable
try:
sticky[clss.__name__].Enabled = enabled
except:
pass
create_callback(CtrlDoubleClick, update)
enable_callback(CtrlDoubleClick, enabled)