Eto.Forms - UI that stays with an object as the viewport moves

Hello,

Another Eto question from me…

I’m attempting to use the WorldToClient method to translate a Point3d location in World Space to 2D Screen Space and spawn an Eto.Form at the location of this point object in World Space.
The point object in this example is the midpoint of an arc curve but I’m trying to understand how to achieve this with any 3d point.

How can I ensure that the Eto UI location is updated in real-time so that it is always at the point3d location even as the viewport/camera moves/rotates/pans?

I’ve added a simple Rhino file and Grasshopper script for a prototype mockup.

Note how the text dot is always at the mid point of the door swing arc. (I realize this uses different location code under the hood, it’s just a visual example of where I want the UI to be)

I would like to replace the text dot with am Eto.Form UI that I can “do things with” but have this form stay visually locked to the original location point as the camera moves. The form would be closed after the object is no longer selected or an action that triggers a close is performed.

Thank you all for your help!

Nothing Selected:

Door “Guiding Point” Selected (Note that text dot appears at arc midpoint):

Eto.Form at object location:

Eto.Form after viewport moved (still at the original location, not the current location of the object):

import Rhino
import scriptcontext as sc
import System

import Eto.Forms as forms
import Eto.Drawing as drawing

sc.doc = Rhino.RhinoDoc.ActiveDoc

# Global variable to hold the form instance
form_instance = None

def GetScreenLocation(Location):
    # Convert world coordinates to screen coordinates
    view = Rhino.UI.RhinoEtoApp.MainWindow.Screen.WorkingArea
    screen_location = view.ActiveViewport.WorldToClient(Location)
    return screen_location

class SampleTransparentEtoForm(forms.Form):
    def __init__(self):

        self.WindowStyle = forms.WindowStyle.None
        
        self.Styles.Add[forms.Panel]("transparent", self.MyFormStyler)
        self.Style = "transparent"
        
        self.AutoSize = False
        self.Resizable = True
        self.TopMost = True
        self.ShowActivated = False
        self.Size = drawing.Size(50, 70)  # Adjust the size as needed
        self.Padding = drawing.Padding(10)
        self.MovableByWindowBackground = False

        # Convert world coordinates to screen coordinates
        view = scriptcontext.doc.Views.ActiveView
        screen_location = view.ActiveViewport.WorldToClient(Location)

        self.Location = drawing.Point(screen_location.X, screen_location.Y)
        
        self.Button = forms.Button(Text=Angle, BackgroundColor=drawing.Colors.Transparent)
        self.TextBox = forms.TextBox(Text=Angle)
        self.TextBox.TextChanged += self.OnTextChanged
        self.TextBox.KeyDown += self.OnTextBoxKeyDown
        self.TextBox.Visible = False
        self.updated_angle = None
        
        # Event handler for button click
        self.Button.Click += self.OnButtonClick
        
        self.layout = forms.DynamicLayout()
        self.layout.AddRow(self.Button)
        self.layout.AddRow(self.TextBox)
        
        # Add ImageView to the layout
        self.layout.AddRow(self.DrawImage())  # Call DrawImage method and add it to the layout
        
        self.Content = self.layout

    def MyFormStyler(self, control):
        self.BackgroundColor = drawing.Colors.Transparent
        window = control.ControlObject
        if hasattr(window, "AllowsTransparency"):
            window.AllowsTransparency = True
        if hasattr(window, "Background"):
            brush = window.Background.Clone()
            brush.Opacity = 0
            window.Background = brush
        else:
            color = window.BackgroundColor
            window.BackgroundColor = color.FromRgba(0,0,0,0)

    def DrawImage(self):
        my_image = forms.ImageView()
        my_image.Size = drawing.Size(10, 10)
        # Use a raw string for the file path or replace \ with \\
        my_image.Image = drawing.Bitmap(r"user set image path")
        return my_image

    def OnTextChanged(self, sender, e):
        # Update button text with the new value from the text box
        self.updated_angle = self.TextBox.Text
        self.Button.Text = self.TextBox.Text
        # Optionally, perform other actions based on the new text value
        print("Text changed:", self.TextBox.Text)

    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == forms.Keys.Enter:
            # Update angle and hide text box when Enter key is pressed
            self.updated_angle = self.TextBox.Text
            self.TextBox.Visible = False
            self.Button.Text = self.TextBox.Text
            print("Angle updated:", self.updated_angle)

    def OnButtonClick(self, sender, e):
        # Show the text box when the button is clicked
        self.TextBox.Visible = True

def DoSomething():
    global form_instance
    
    # Check if form instance exists and clean it up
    if form_instance:
        form_instance.Cleanup()

    # Create new instance of the form
    form_instance = SampleTransparentEtoForm()
    form_instance.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    form_instance.Show()

if __name__=="__main__":
    DoSomething()

sc.doc = ghdoc

-Do I just need to enable a timer function to continually update the location?
-Can I subscribe to a Viewport Update/Changed event for this?
-How do I prevent the form form from continually spawning a duplicate of itself every time the location updates?

20240317_Door_Angle_UI_Test_Environment.3dm (285.0 KB)
20240317_Door_Angle_UI_Control_Test_01a.gh (43.1 KB)

1 Like

Hi @michaelvollrath, what kind of things ?

I’m asking since i do have the feeling that it’s difficult to make a form follow the 3d point in the viewport, especially when you have multiple views to compute the 2d screen point from it.

If you go this route, better is to subscribe to an Idle event and then try to update. A conduit of the point would probably update itself when the camera changes.

You might put your form in the sticky and query if it is already open to prevent that another one is opened. Then update the location of the existing one.

c.

I would like to have a form that could have buttons, images, sliders, etc. essentially the full Eto.Forms functionality but persistently updating its Location variable with the 3d point.

It would/should only occur in the active viewport or even just the perspective viewport perhaps.

Good idea, I’ll go that route!

I found this Viewport Property, ill try here as this may be enough since it only needs to update the Location if the viewport camera has changed .

https://developer.rhino3d.com/api/rhinocommon/rhino.display.rhinoviewport/changecounter

Thanks, as always for your help :pray: @clement