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:
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
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.
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.
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?