Lags with mouse tracking on RhinoBeta

Hello,

I am developing a « Move Normal to Surface » definition including a Grasshopper Python script.

It is integrated in a larger definition aimed at facilitating jewelry design.

Thanks to this post and this other post I could make a definition running smoothly on Rhino 8.

import Rhino
import Grasshopper
from ghpythonlib.componentbase import executingcomponent as component


class MyMouseCallback(Rhino.UI.MouseCallback):

    def __init__(self):
        super().__init__()
        self.move = None

    def OnMouseMove(self, arg : Rhino.UI.MouseCallbackEventArgs):
        self.move = arg
        ghenv.Component.OnPingDocument().ScheduleSolution(
            1,
            ghenv.Component.ExpireSolution(True)
        )
 
 
class MyComponent(component):

    def RunScript(self):
        return self.getFrustrumLine()

    def getFrustrumLine(self):
        if self.mouse.move:

            mousePoint = self.mouse.move.ViewportPoint
            frustrumLine = self.mouse.move.View.ActiveViewport.ClientToWorld(mousePoint)
            return frustrumLine

    def __enter__(self):
        self.mouse = MyMouseCallback()
        self.mouse.Enabled = True

    def __exit__(self):
        self.mouse.Enabled = False
        del self.mouse

move_normal_to_surface.gh (10.2 KB)

However I am experiencing a loss in fluidity when using the same definition on RhinoBeta : the cursor tracking moves slowly and lags.

https://youtu.be/1yp0nBytNgk

I am using a MacBook Pro.

Could you please let me know why this issue is happening and if there is a way to solve it ?

Hi @François_Farnault,

Does this work any better?

move_normal_to_surface.gh (29.0 KB)

– Dale

Hello @Dale_Fugier

Yes It does work better. Thank you.

There is still a remnant lag that makes me wonder if there would be a better approach.

Also I did not yet managed to implement this solution in a script with different mouse events that can trigger solution expiration.

Today is the first day at school so I will work on this next weekend.

For context I am a teacher.

I might have this completely wrong but I think you added a flag to ensure that only one scheduling can happen during RunScript.

The confusing part to me is how and when this multiple scheduling could have happened.

You are giving me an opportunity to learn more about callbacks and document expiration.

Thank you again for your help.

v1_dale_2

Hello, here is some progress.

I asked Claude to help clarify the sequence of events.

I had initially mistaken _enter_ and _exit_ for BeforeRunScript and AfterRunScript.

However, _enter_ and _exit_ are called once per component instance lifecycle (when the document is opened/closed, or when the component is added/deleted).

By contrast, BeforeRunScript and AfterRunScript are called on every solution.

We adapted @Dale_Fugier ’s solution to a few other events, including recording a mouse click, which makes this kind of “bake on click” interaction possible:

bake_on_click

import Rhino
import Grasshopper
from ghpythonlib.componentbase import executingcomponent as component


class MyMouseCallback(Rhino.UI.MouseCallback):

    def __init__(self):
        super().__init__()
        self.move = None
        self.click = None
        self.pending = False

    def OnMouseMove(self, arg):
        # Store the raw mouse event
        # This data is only read later, when Grasshopper actually solves.
        self.move = arg
        self._request_solution()

    def OnMouseUp(self, arg):
        # Only register left-click to avoid triggering on right-click view rotation.
        if arg.MouseButton == Rhino.UI.MouseButton.Left:
            self.click = True
            self._request_solution()

    def _request_solution(self):
        # Avoid piling up multiple solve requests: one pending at a time.
        if not self.pending:
            self.pending = True
            ghenv.Component.OnPingDocument().ScheduleSolution(5, self.solve)

    def solve(self, doc):
        self.pending = False
        ghenv.Component.ExpireSolution(False)

    def consume_click(self):
        # One-shot read: returns the click state then resets it to False.
        click = self.click
        self.click = False
        return click


class MyComponent(component):

    def RunScript(self):
        frustrum_line = self._getFrustrumLine()
        click = self.mouse.consume_click()
        return frustrum_line, click

    def _getFrustrumLine(self):
        if self.mouse.move:
            mousePoint = self.mouse.move.ViewportPoint
            return self.mouse.move.View.ActiveViewport.ClientToWorld(mousePoint)

    def __enter__(self):
        self.mouse = MyMouseCallback()
        self.mouse.Enabled = True

    def __exit__(self):
        self.mouse.Enabled = False
        del self.mouse

move_normal_to_surface_bakeOnClick.gh (6.7 KB)

We somehow managed to isolate the source of the lag by removing ScheduleSolution() from the script and using Redraw() and DrawViewportWires() instead.

import Rhino
import Grasshopper
import scriptcontext
import System.Drawing
from ghpythonlib.componentbase import executingcomponent as component


class MyMouseCallback(Rhino.UI.MouseCallback):

    def __init__(self):
        super().__init__()
        self.move = None

    def OnMouseMove(self, arg):
        # Store the raw mouse event, then force an immediate redraw.
        # DrawViewportWires reads this directly on every redraw.
        self.move = arg
        scriptcontext.doc.Views.Redraw()


class MyComponent(component):

    def RunScript(self,
            targetSurface: Rhino.Geometry.Surface,
            objectToOrient: Rhino.Geometry.Brep):
        self.targetSurface = targetSurface
        self.objectToOrient = objectToOrient

    def _getHitFrame(self):
        if not self.mouse.move or self.targetSurface is None:
            return None

        mousePoint = self.mouse.move.ViewportPoint
        frustrumLine = self.mouse.move.View.ActiveViewport.ClientToWorld(mousePoint)

        t = Rhino.Geometry.Intersect.Intersection.CurveSurface(
            frustrumLine.ToNurbsCurve(), self.targetSurface, 0.001, 0.001
        )
        if t is None or t.Count == 0:
            return None

        closestHit = t[t.Count - 1]
        u, v = closestHit.SurfacePointParameter()
        success, frame = self.targetSurface.FrameAt(u, v)
        return frame if success else None

    def _getOrientedObject(self):
        hitFrame = self._getHitFrame()
        if hitFrame is None or self.objectToOrient is None:
            return None

        xform = Rhino.Geometry.Transform.PlaneToPlane(Rhino.Geometry.Plane.WorldXY, hitFrame)
        oriented = self.objectToOrient.Duplicate()
        oriented.Transform(xform)
        return oriented

    def DrawViewportWires(self, arg):
        orientedObject = self._getOrientedObject()
        if orientedObject is not None:
            arg.Display.DrawBrepShaded(orientedObject, self.material)

    def __enter__(self):
        self.mouse = MyMouseCallback()
        self.mouse.Enabled = True
        self.material = Rhino.Display.DisplayMaterial(System.Drawing.Color.Blue)

    def __exit__(self):
        self.mouse.Enabled = False
        del self.mouse

Something interesting happens:

  • Objects drawn with DrawViewportWires() do not lag.
  • Objects drawn through the component preview after ScheduleSolution() do lag.

lag

In both cases, a MouseCallback is used, so I don’t think the issue comes from the mouse callback itself. This example seems to points at the solve mechanism itself as the source of the lag on RhinoBETA.

move_normal_to_surface_DrawViewportWires.gh (13.2 KB)

I have tried this on Rhino8 Mac and the lag is barely perceptible.

The lag is still present in RhinoBeta, however, and I am concerned that it could lead to an unpleasant user experience.

My aim is to use this definition as a teaching example for students.

I could route the interactive logic through DrawViewportWires(), but my students primarily work by connecting native Grasshopper components rather than writing Python code.

For an optimal teaching experience, I would like to keep the Python scripting to a minimum.

Is this a known limitation in RhinoBETA that might be addressed?

Or is there another recommended approach for achieving smooth live interactivity while relying mostly on native Grasshopper components?