Continuous MouseCallback in GH

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?

Many thanks in advance for a hint :wink:

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.

Hi, I tried to implement what you suggested, however, OnMouseDown is never called in my code. Do you have any ideas why this could be the case?

Thanks a lot!
Kris

from ghpythonlib.componentbase import executingcomponent as component
import Grasshopper, GhPython
import System
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
import threading
import time     

class MCB_Singleton:
    
    instance = None
    
    class MyMouseCallback(Rhino.UI.MouseCallback):
        def __init__(self):
            super(MCB_Singleton.MyMouseCallback, self).__init__()
            self.on_mcbs = []
            
        def register_on_mouse_down_callback(self, cb):
            self.on_mcbs.append(cb)
            
        def OnMouseDown(self, args):
            for cb in self.on_mcbs:
                cb()
                
    def __init__(self):
        if MCB_Singleton.instance is None:
            MCB_Singleton.instance = MCB_Singleton.MyMouseCallback()
            
    def __getattr__(self, name):
        return getattr(self.instance, name)

j = 0

def test():
    global j
    j += 1

class MyComponent(component):
    
    def __init__(self):
        self.do_mcb = True
        self.thread = threading.Thread(target=self.start_mcb)
        self.thread.start()

    def start_mcb(self):
        import time
        import Rhino
        sc.sticky["mcb"] = MCB_Singleton()
        sc.sticky["mcb"].Enabled = True
        sc.sticky["mcb"].register_on_mouse_down_callback(test)
        while self.do_mcb:
            Rhino.RhinoApp.Wait()
        sc.sticky["mcb"].Enabled = False

    def stop_mcb(self):
        self.do_mcb = False
        self.thread.join(timeout=1)
      
    def RunScript(self, update):

        print j, self.thread.is_alive(), sc.sticky["mcb"].Enabled, sc.sticky["mcb"].on_mcbs
        
        time.sleep(0.1)
        if update:
            ghenv.Component.ExpireSolution(True)

        return j

    def __del__(self):
        print "del"
        self.stop_mcb()

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…

I finally found a plugin that allows for continuous mouse and keyboard feedback https://www.food4rhino.com/app/interactool

Best,
Kris

1 Like

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)

Hope it helps :slight_smile:

3 Likes