Did I hit my first limitation for utilizing scripts?

Hello All,
I was able to get this script to read a point coordinate and generate a dot object that reports the. z value of this coordinate. Before compiling it to a rhp plugin. I’m wondering I it is possible to keep the dot “Alive” so every time I move the point it will automatically report the z.value. unless the idea of developing a plugin from a script is too linear and limited.

Is there a method that tracks (or listens) to the change to object properties (in this case position), and reevaluate the function?

I replicated my intended function in Grasshopper, like the below:
Screencap_2.V8

Below is my starting point

import Rhino
import scriptcontext
import System

def AddDot():
    # Get the point from the user
    rc, point = Rhino.Input.RhinoGet.GetPoint("Pick a point location", False)
    if rc != Rhino.Commands.Result.Success:
        return rc

    # Get the active view's construction plane
    view = scriptcontext.doc.Views.ActiveView
    if view:
        cplane = view.ActiveViewport.ConstructionPlane()
        # Calculate the distance from the point to the construction plane
        z_value = 12 * cplane.DistanceTo(point)
        # Convert to feet and fractional inches
        feet, inches = divmod(z_value, 12)
        inches, fraction = divmod(inches, 1)
        fraction = round(fraction * 16) / 16
        if fraction.is_integer():
            inches += int(fraction)
            fraction = 0
        # Format the output
        if fraction > 0:
            output = "{}' {} {}/16\"".format(int(feet), int(inches), int(fraction * 16))
        else:
            output = "{}' {}\"".format(int(feet), int(inches))
        # Add a dot with the formatted output
        dot = Rhino.Geometry.TextDot(output, point)
        
        # Find or create the layer called "+WorkArea::Elev"
        layer = scriptcontext.doc.Layers.FindName("+WorkArea::Elev")
        if not layer:
            layer_index = scriptcontext.doc.Layers.Add("+WorkArea::Elev", System.Drawing.Color.Black)
            layer = Rhino.DocObjects.Layer()
            layer.Index = layer_index
        
        # Add the dot to the layer
        attributes = Rhino.DocObjects.ObjectAttributes()
        attributes.LayerIndex = layer.Index
        scriptcontext.doc.Objects.AddTextDot(dot, attributes)
        
        scriptcontext.doc.Views.Redraw()
    return Rhino.Commands.Result.Success

if __name__ == "__main__":
    AddDot()

I don’t know how to solve your problem but your idea got me thinking.

What if a text dot display text could be set like a text field?
Maybe that’s already possible but I couldn’t get it to work…

2 Likes

Maybe ondraw or ontransform?

https://developer.rhino3d.com/api/rhinocommon/rhino.docobjects.rhinoobject/ontransform

1 Like

I wasn’t sure if Rhino made it easy to receive event notification in Python (the github samples for RhinoCommon/py are an empty placeholder).

But I found an example for you at
Rhino - Eto Controls in Python (rhino3d.com)

which shows how to bind a mouse click event to a particular Python call. That example plus the Rhino Common API docs and the C# or VB examples will hopefully be enough.

If I recall correctly, you’ll need to watch for the deletion and recreation events on updates to objects based on ReplaceRhinoObject.

See, for example (someone else detecting movement of a point), Managing ReplaceRhinoObject event - Rhino Developer - McNeel Forum

(assuming all of that is supported to connect Python to Rhino Common)

2 Likes

Yes basically anything in RhinoCommon should be available via python. You may need to translate the examples from C# but it should all be there. I also sometimes use the rhinoscriptsyntax module source code for help finding the RhinoCommon methods

1 Like

this example (event watching / eventhander) is your starting-point

2 Likes

I quite like that possibility

1 Like

@martinsiegrist @michaelvollrath
This is supposed to be an available functionality. and my first attempt at getting this to wok, unfortunately I found myself forced to get into python scripting to get some basic functionality.
How to? Embed Z coordinate usertext in a block - Rhino - McNeel Forum

We are Getting there, now I’m focusing on getting the event handler to work for me, stay tuned. :slight_smile:
and this rhinopython page on github is gold!

It’s not too difficult. Just subscribe to events like mentioned in the sample. Best practice is to put anything you want to do in an Idle processor.

@Tom_P @Nathan_Bossett and @Dancergraham

I was able to get a transformation event handler to work, next step is to build it as a class and use it to track any transformation that may occur in the model, then filter the change to reflect the new z value of the dot’s coordinates.

Thank you for your Guidance.

import Rhino
import scriptcontext

def onTransform(sender, e):
    if e.OldRhinoObject.Geometry != e.NewRhinoObject.Geometry:
        print("object moved")

def testTransformObjectHandler():
    Rhino.RhinoDoc.ReplaceRhinoObject += onTransform

testTransformObjectHandler()
1 Like

Hello All,

Here is an update:
I was able to make it work in a very inefficient way. My biggest struggle is that every time I update the TextDot’s Text value. it will be triggered as a Geometric change that causes eternal loop of recursive events.

Screencap_2.V8

I managed to get around the issue by simply deleting the old dot and creating a new one and assign the new attributes to it, didn’t sound right to me, that’s why my question below:

Is there any way to replace a TextDot’s Text value without triggering that event? I tried different methods? I’d love to hear from people with experience.

# Create a function to handle the TransformObject event
def onTransform(sender, e):
    # Filter out all objects except text dots
    if isinstance(e.OldRhinoObject.Geometry, Rhino.Geometry.TextDot):
        new = e.NewRhinoObject
        old = e.OldRhinoObject
        # get the z values of the new and old dots
        new_z = round(new.Geometry.Point[2], 3)
        old_z = round(old.Geometry.Point[2], 3)
        if new_z != old_z:
             # get the new objects
            new = e.NewRhinoObject
            # convert Guid to RhinoObject
            objgeo = new.Geometry
            # Get the dot's grip point
            point = objgeo.Point
            # get the coordinate system
            cplane = get_coordinate_system()
            # Calculate the distance from the point to the construction plane
            z_value = 12 * cplane.DistanceTo(point)
            # Convert the distance to feet and inches
            output = decimal_to_feet_and_inches(z_value)
            # delete the old dot
            rs.DeleteObject(old)
            # create a new dot
            dot = Rhino.Geometry.TextDot(output, point)
            attributes = Rhino.DocObjects.ObjectAttributes()
            # add the elevation to the dot
            attributes.SetUserString("Elevation", output)
            # add the dot to the layer "+WorkArea::Elev"
            layer = scriptcontext.doc.Layers.FindName("+WorkArea::Elev")
            # assign layer to the dot
            attributes.LayerIndex = layer.Index
            # add the dot to the document
            scriptcontext.doc.Objects.AddTextDot(dot, attributes)
            # redraw the view
            scriptcontext.doc.Views.Redraw()

Attached is my script if you are interested in constructive feedback. My goal is to have no repeat code and more streamlined functions to set up a project’s Cplane that serves as a construction datum for my projects.

Hi @tay.othman,

The delete/add method of trying to show objects changing dynamically is really inefficient and fills the undo stack with unwanted stuff. Better to just draw the changes dynamically.

For example:

import Rhino
import scriptcontext as sc

class get_textdot_point(Rhino.Input.Custom.GetPoint):
    def OnDynamicDraw(self, e):
        point = e.CurrentPoint
        e.Display.DrawDot(point, point.Z.ToString())
        Rhino.Input.Custom.GetPoint.OnDynamicDraw(self, e)    

def test_move_textdot():
    filter = Rhino.DocObjects.ObjectType.TextDot
    rc, objref = Rhino.Input.RhinoGet.GetOneObject("Select text dot to move", False, filter)
    if not objref or rc != Rhino.Commands.Result.Success:
        return 
    
    text_dot = objref.TextDot()
    if not text_dot:
        return
        
    gp = get_textdot_point()
    gp.SetCommandPrompt("Point to move to")
    gp.SetBasePoint(text_dot.Point, True)
    gp.DrawLineFromPoint(text_dot.Point, True)
    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return
        
    point = gp.Point()
    new_dot = text_dot.Duplicate()
    new_dot.Point = point
    new_dot.Text = point.Z.ToString()
    
    sc.doc.Objects.Replace(objref, new_dot)
    sc.doc.Views.Redraw()

if __name__ == "__main__":
    test_move_textdot()

– Dale

Thank you Dale,
The Dynamic Draw is very nice, I’m planning to include this.
no my question is how to include the dynamic draw in the event handler without putting my machine in a recursive loop every time the dot value changes?

I’m not trying to get into your problem deep, but by quickly reading the source code, I saw that you are doing something dangerous here. Whenever you add an event-handler using the ‘+=’ operator, you really need to make sure to either remove it from the same script instance using ‘-=’ operator or you need to close Rhino. When you speak of “recursive” issues, there is a chance that you simply have multiple handlers active. Event handling from a script is dangerous, because you cannot remove the handler, if you lost the function pointer to your current handler. This is because ,whenever you execute a script, you are basically recompiling it with different name-spaces and addresses for its functions.

You can however store the eventhandler function pointer into a global dictionary. For Rhino Python, there is the “sticky” dictionary.

1 Like

Thank you Tom.

I’m looking into this really something foreign for me. But I’ll construct a handlers per your suggestions. Your point makes sense.

Hi @Tay.0

This should resolve some of your issues, keep in mind that your are adding tons of garbage without properly handling it.
I think that encapsulating in a class and attaching/detaching the handler is a safer approach

import Rhino
import scriptcontext

class TransformEventHandler:
    def __init__(self):
        self.is_attached = False

    def onTransform(self, sender, e):
        try:
            if e.OldRhinoObject.Geometry != e.NewRhinoObject.Geometry:
                print("object moved")
        except Exception as ex:
            print("An error occurred:"+ex)

    def attach(self):
        if not self.is_attached:
            Rhino.RhinoDoc.ReplaceRhinoObject += self.onTransform
            self.is_attached = True

    def detach(self):
        if self.is_attached:
            Rhino.RhinoDoc.ReplaceRhinoObject -= self.onTransform
            self.is_attached = False

# Create an instance of the event handler and store it in scriptcontext.sticky
key = 'transform_event_handler'
if key not in scriptcontext.sticky:
    scriptcontext.sticky[key] = TransformEventHandler()

# Attach or reattach the event handler
handler = scriptcontext.sticky[key]
handler.detach()
handler.attach()

Hope this helps
Farouk

2 Likes

Thank you @farouk.serragedine this is a great deal of help, I have a question, can I call out a local function (definition) from the “encapsulated class”?