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:
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()
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.
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
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.
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.
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.
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()
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.
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()
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”?