GUI - Rhino - Radial Menu - Cross Platform

Hi @clement,

From your suggestion and more research, I was able to update my previous code to use only a form and not a dialog now.

It’s much easier to manage now, thanks for that.

I managed to get some graphics working, dynamic highlight, etc. and associate the “buttons” with Rhino commands.

Also now that I’m not doing the double dialog/form situation, the UI shows up on the Mouse Cursor position as expected so that’s great!

I’m now trying to implement the mouse event handling from the Form itself because in it’s current implement, despite being transparent, the “flick highlighting” only starts working when I first draw off of the form. If I am on the form after spawn with the mouse, it doesn’t work oddly. So I’m trying to sort that right now but otherwise it’s generally working well.

EDIT:
Silly me… I forgot I changed the form transparency to “visible” despite not being able to visually see it. So the mouse was tracking against the form despite me not wanting it to. I made it completely invisible again and that fixed it. (I updated the code below to reflect this)

Here’s a video of progress, for now I’m associating custom images and commands for testing but as mentioned earlier, I would like to make this user customizable at some point.

Code Thus Far:

import Eto.Forms as forms
import Eto.Drawing as drawing
import System
import Rhino.UI
import math

class CircleProperties:
    def __init__(self, icon_path=""):
        self.fill_color = drawing.Colors.White
        self.border_color = None  # Will be set dynamically
        self.highlight_color = drawing.Colors.Orange
        self.icon_path = icon_path  # Icon path attribute with a default value of ""

class TransparentForm(forms.Form):
    def __init__(self):
        self.Title = "PIE Menu 0.1"
        self.WindowStyle = forms.WindowStyle.None
        self.AutoSize = False
        self.Resizable = False
        self.TopMost = True
        self.ShowActivated = False
        self.Size = drawing.Size(300, 300)
        self.Padding = drawing.Padding(20)
        self.MovableByWindowBackground = False

        # Initialize circle properties list with icon paths
        icon_paths = [
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        "user_set_path",
        ]
        self.circle_properties = [CircleProperties(icon_path) for icon_path in icon_paths]

        self.form_bitmap = drawing.Bitmap(drawing.Size(300, 300), drawing.PixelFormat.Format32bppRgba)
        self.DrawCirclesOnBitmap(self.form_bitmap, None)

        self.image_view = forms.ImageView()
        self.image_view.Image = self.form_bitmap

        layout = forms.DynamicLayout()
        layout.AddRow(self.image_view)
        self.Content = layout

        self.selected_button_index = None  # Initialize selected button index variable

        # Add transparent style
        self.Styles.Add[forms.Panel]("transparent", self.MyFormStyler)
        self.Style = "transparent"
        
    def DrawCirclesOnBitmap(self, bitmap, pointed_circle_index):
        # Clear the bitmap
        graphics = drawing.Graphics(bitmap)
        graphics.Clear(drawing.Colors.Transparent)

        circle_radius = 100
        num_circles = 8
        button_radius = 35

        center_x = bitmap.Size.Width / 2
        center_y = bitmap.Size.Height / 2

        for i, properties in enumerate(self.circle_properties):
            angle = (2 * math.pi / num_circles) * i
            circle_center_x = center_x + circle_radius * math.cos(angle)
            circle_center_y = center_y + circle_radius * math.sin(angle)
            circle_position = drawing.Point(circle_center_x - button_radius, circle_center_y - button_radius)
            circle_size = drawing.Size(2 * button_radius, 2 * button_radius)
          
            circ_brush = drawing.SolidBrush(properties.fill_color)  # Use circle properties

            highlight_color = drawing.Colors.Orange  # Set the highlight color
            border_color = drawing.Colors.LightGrey  # Assign a unique border color for each circle
            
            if i == pointed_circle_index:
                border_pen = drawing.Pen(highlight_color, 10)
                self.selected_button_index = i  # Update selected button index
            else:
                border_pen = drawing.Pen(border_color, 5)

            graphics.FillEllipse(circ_brush, drawing.Rectangle(circle_position, circle_size))
            graphics.DrawEllipse(border_pen, drawing.Rectangle(circle_position, circle_size)) 
            # Draw the image
            icon_bitmap = drawing.Bitmap(properties.icon_path)
            graphics.DrawImage(icon_bitmap, circle_center_x - 25, circle_center_y - 25, 50, 50)

        graphics.Dispose()

    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  # Adjust opacity as needed
            window.Background = brush
        else:
            color = window.BackgroundColor
            window.BackgroundColor = color.FromRgba(0, 0, 0, 0)

    def Cleanup(self):
        # Close the form if it's shown
        if self.Visible:
            self.Close()
        
        # Dispose of the form's resources
        self.Dispose()

class MyMouseCallback(Rhino.UI.MouseCallback):
    def __init__(self):
        self.original_position = None
        self.form = None
        self.mouse_moved = False

    def OnMouseDown(self, e):
        if e.Button == System.Windows.Forms.MouseButtons.Middle:
            Rhino.RhinoApp.WriteLine("MMB Down")
            if self.form:
                self.form.Cleanup()  # Clean up any existing UI
            self.original_position = Rhino.UI.MouseCursor.Location
            self.form = TransparentForm()
            form_loc = drawing.Point(self.original_position.X - 150, self.original_position.Y - 150)
            Rhino.RhinoApp.WriteLine(str(form_loc))
            self.form.Location = form_loc
            self.form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
            self.form.Show()

    def OnMouseMove(self, e):
        if self.original_position:
            current_position = Rhino.UI.MouseCursor.Location
            
            if current_position != self.original_position:
                self.mouse_moved = True

                vector_x = current_position.X - self.original_position.X
                vector_y = current_position.Y - self.original_position.Y

                angle = math.atan2(vector_y, vector_x)
                angle_degrees = math.degrees(angle)
                
                if angle_degrees < 0:
                    angle_degrees += 360
                
                num_circles = 8
                pointed_circle_index = int(angle_degrees / (360 / num_circles))
                # Rhino.RhinoApp.WriteLine("Button Index: "+ str(pointed_circle_index) )

                # Create a new bitmap for highlighting
                highlight_bitmap = drawing.Bitmap(drawing.Size(300, 300), drawing.PixelFormat.Format32bppRgba)
                # Call the DrawCirclesOnBitmap method with the highlighted circle
                self.form.DrawCirclesOnBitmap(highlight_bitmap, pointed_circle_index)
                # Update the ImageView with the new highlight_bitmap
                self.form.image_view.Image = highlight_bitmap

    def OnMouseUp(self, e):
        if e.Button == System.Windows.Forms.MouseButtons.Middle:
            if self.mouse_moved:
                self.original_position = None
                if self.form:
                    selected_index = self.form.selected_button_index
                    # Close the form first
                    self.form.Close()
                    self.form = None
                    if selected_index is not None:
                        # Execute the corresponding Rhino command
                        commands = ["Floor", "Wall", "Door", "Window", "Electrical", "Plumbing", "Object", "Room"]
                        Rhino.RhinoApp.WriteLine("Running {} tool...".format(commands[selected_index]))
                        if selected_index < len(commands):
                            Rhino.RhinoApp.RunScript(commands[selected_index], False)
                        else:
                            Rhino.RhinoApp.WriteLine("No command found for index {}.".format(selected_index))
            elif self.form:
                # If mouse hasn't moved, close the form
                self.form.Close()
                self.form = None


def DoSomething():
    if scriptcontext.sticky.has_key('MyMouseCallback'):
        callback = scriptcontext.sticky['MyMouseCallback']
        if callback:
            callback.Enabled = False
            callback = None
            scriptcontext.sticky.Remove('MyMouseCallback')
    else:
        callback = MyMouseCallback()
        callback.Enabled = True
        scriptcontext.sticky['MyMouseCallback'] = callback
        Rhino.RhinoApp.WriteLine("Click somewhere...")

if __name__ == "__main__":
    DoSomething()

Thanks for all your help, I’m really learning a lot in this exercise!

15 Likes