diff --git a/README.md b/README.md index d949afb..b703672 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ -# Visual-Auton-Generator -GUI to Path Autons and generate ARMS code +# Machine Autonomous Planner + +The **Machine Autonomous Planner**, or **MAP**, is a GUI used to plan out and generate ARMS movement code for autonomous routes use in the VEX Robotics Competition. + +## How does MAP work? + +MAP allows VRC teams to completely design the movement part of an autonomous route in just a few clicks with an **intuitive graphical interface**. + +Once the user feels done with the route, they can generate the needed ARMS code in a separate file, ready for them to place in their PROS project and tune as needed. + +## What is ARMS? + +**ARMS** is a template for PROS developed by the Purdue SIGBots software subteam. Its goal is to streamline the development of robot movement code by providing easy-to-use functions for beginner programmers as well as access to odometry and PID control for more advanced teams. More information about how to install and use ARMS can be found [here](https://github.com/purduesigbots/ARMS). \ No newline at end of file diff --git a/classes/movement.py b/classes/movement.py index e400d0d..db5068e 100644 --- a/classes/movement.py +++ b/classes/movement.py @@ -1,30 +1,69 @@ # import statements +from abc import abstractmethod from classes.converter import Converter as c from classes.constants import DARK_MODE_BG, DARK_MODE_FG, LIGHT_MODE_BG, LIGHT_MODE_FG, SELECTED_COLOR import tkinter as tk import math -# Movement class encapsulates code associated with movements +# Movement class encapsulates code associated with linear movements class Movement: - def __init__(self, owner, index, start, end, line_ref, name="Movement"): + def __init__(self, owner, index, name): # initialize variables self.owner = owner self.index = index - self.line_ref = line_ref - self.start = start - self.end = end + self.name = name self.options = { "speed": 100, "flags": { "arms::ASYNC": False, "arms::RELATIVE": False, - "arms::BACKWARDS": False, + "arms::REVERSE": False, "arms::THRU": False, } } - self.name = name + self.selected = False + + # handles mouse click input for movement + @abstractmethod + def click_handler(self, event): + pass + + # clear arrow from canvas + @abstractmethod + def clear(self): + pass + + # draw arrow on canvas + @abstractmethod + def draw(self): + pass + + # set the speed to val + def set_speed(self, val): + self.options["speed"] = val + + # get name as comment + def get_name_as_cmt(self): + return f'// {self.name}\n' + + # get string for exporting to script + @abstractmethod + def to_string(self): + pass + +# Linear class encapsulates code associated specifically with linear movements +class Linear(Movement): + + def __init__(self, owner, index, name, start, end, line_ref): + # call super + super().__init__(owner, index, name) + + # initialize variables + self.start = start + self.end = end + self.line_ref = line_ref # bind click handler to line_ref tag self.owner.canvas.tag_bind(self.line_ref, "", self.click_handler) @@ -60,22 +99,72 @@ def draw(self): # rebind click handler self.owner.canvas.tag_bind(self.line_ref, "", self.click_handler) - # set the speed to val - def set_speed(self, val): - self.options["speed"] = val - - # toggle async value - def toggle_async(self): - self.options["flags"]["arms::ASYNC"] = not self.options["flags"]["arms::ASYNC"] + # get string for exporting to script + def to_string(self): + x = c.convert_x(self.end[0]) + y = c.convert_y(self.end[1]) + + # if movement is relative + if self.options["flags"]["arms::RELATIVE"]: + start_x = c.convert_x(self.start[0]) + start_y = c.convert_y(self.start[1]) + + # make the x and y the difference between end and start + x -= start_x + y -= start_y + + joined_flags = " | ".join([f for f in self.options["flags"] if self.options["flags"][f]]) + return f'chassis::move({{{{{round(x, 2)}, {round(y, 2)}}}}}, {self.options["speed"]}{", " + joined_flags if len(joined_flags) > 0 else ""});\n' + +# Linear class encapsulates code associated specifically with angular movements +class Angular(Movement): + + def __init__(self, owner, index, name, origin, start_angle, extent, line_ref): + # call super + super().__init__(owner, index, name) + + # initialize variables + self.origin = origin + self.start_angle = start_angle + self.extent = extent + self.line_ref = line_ref + + # bind click handler to line_ref tag + self.owner.canvas.tag_bind(self.line_ref, "", self.click_handler) - # get name as comment - def get_name_as_cmt(self): - return f'// {self.name}\n' + # handles mouse click input for movement + def click_handler(self, event): + # if the movement is selected and the canvas is not editing + if self.selected and self.owner.editing_movement == 0: + self.owner.editing_movement = 2 + + # set index and clear current line + self.owner.editing_index = self.index + self.clear() + + # clear arrow from canvas + def clear(self): + self.owner.canvas.delete(self.line_ref) + + # draw arrow on canvas + def draw(self): + line_fill = "red" if self.selected else "magenta" + self.line_ref = self.owner.canvas.create_arc(self.origin[0] - 20, self.origin[1] - 20, self.origin[0] + 20, self.origin[1] + 20, + outline=line_fill, start=self.start_angle, extent=self.extent, width=5, style=tk.ARC) + + # rebind click handler + self.owner.canvas.tag_bind(self.line_ref, "", self.click_handler) # get string for exporting to script def to_string(self): + # if movement is relative, then turn angle is only extent + if self.options["flags"]["arms::RELATIVE"]: + angle = self.extent + else: + angle = self.start_angle + self.extent + joined_flags = " | ".join([f for f in self.options["flags"] if self.options["flags"][f]]) - return f'chassis::move({{{c.convert_x(self.end[0])} , {c.convert_y(self.end[1])}}}, {self.options["speed"]}{", " + joined_flags if len(joined_flags) > 0 else ""});\n' + return f'chassis::turn({round(angle, 2)}, {self.options["speed"]}{", " + joined_flags if len(joined_flags) > 0 else ""});\n' # SidebarGroup class encapsulates sidebar widget groups class SidebarGroup: @@ -123,15 +212,15 @@ def __init__(self, movement, owner, index, speed=100, flags=""): self.relative_flag.set(True) self.relative_checkbox.select() - # create backwards flag variable and checkbutton - self.backwards_flag = tk.BooleanVar() - self.backwards_checkbox = tk.Checkbutton(self.frame, text="BACKWARDS", variable=self.backwards_flag, onvalue=True, offvalue=False, command=self.set_flags) - self.backwards_checkbox.grid(row=1, column=2, columnspan=1) + # create reverse flag variable and checkbutton + self.reverse_flag = tk.BooleanVar() + self.reverse_checkbox = tk.Checkbutton(self.frame, text="REVERSE", variable=self.reverse_flag, onvalue=True, offvalue=False, command=self.set_flags) + self.reverse_checkbox.grid(row=1, column=2, columnspan=1) - # if imported movement has backwards flag, set to true - if "arms::BACKWARDS" in flags: - self.backwards_flag.set(True) - self.backwards_checkbox.select() + # if imported movement has reverse flag, set to true + if "arms::REVERSE" in flags: + self.reverse_flag.set(True) + self.reverse_checkbox.select() # create thru flag variable and checkbutton self.thru_flag = tk.BooleanVar() @@ -168,7 +257,7 @@ def click_handler(self, event): def set_flags(self): self.movement.options["flags"]["arms::ASYNC"] = self.async_flag.get() self.movement.options["flags"]["arms::RELATIVE"] = self.relative_flag.get() - self.movement.options["flags"]["arms::BACKWARDS"] = self.backwards_flag.get() + self.movement.options["flags"]["arms::REVERSE"] = self.reverse_flag.get() self.movement.options["flags"]["arms::THRU"] = self.thru_flag.get() # select sidebar and movement @@ -220,10 +309,10 @@ def adjust_theme(self): self.relative_checkbox.configure(fg=DARK_MODE_FG) self.relative_checkbox.configure(selectcolor=DARK_MODE_BG) self.relative_checkbox.configure(activebackground=DARK_MODE_BG) - self.backwards_checkbox.configure(bg=DARK_MODE_BG) - self.backwards_checkbox.configure(fg=DARK_MODE_FG) - self.backwards_checkbox.configure(selectcolor=DARK_MODE_BG) - self.backwards_checkbox.configure(activebackground=DARK_MODE_BG) + self.reverse_checkbox.configure(bg=DARK_MODE_BG) + self.reverse_checkbox.configure(fg=DARK_MODE_FG) + self.reverse_checkbox.configure(selectcolor=DARK_MODE_BG) + self.reverse_checkbox.configure(activebackground=DARK_MODE_BG) self.thru_checkbox.configure(bg=DARK_MODE_BG) self.thru_checkbox.configure(fg=DARK_MODE_FG) self.thru_checkbox.configure(selectcolor=DARK_MODE_BG) @@ -244,10 +333,10 @@ def adjust_theme(self): self.relative_checkbox.configure(fg=LIGHT_MODE_FG) self.relative_checkbox.configure(selectcolor=LIGHT_MODE_BG) self.relative_checkbox.configure(activebackground=LIGHT_MODE_BG) - self.backwards_checkbox.configure(bg=LIGHT_MODE_BG) - self.backwards_checkbox.configure(fg=LIGHT_MODE_FG) - self.backwards_checkbox.configure(selectcolor=LIGHT_MODE_BG) - self.backwards_checkbox.configure(activebackground=LIGHT_MODE_BG) + self.reverse_checkbox.configure(bg=LIGHT_MODE_BG) + self.reverse_checkbox.configure(fg=LIGHT_MODE_FG) + self.reverse_checkbox.configure(selectcolor=LIGHT_MODE_BG) + self.reverse_checkbox.configure(activebackground=LIGHT_MODE_BG) self.thru_checkbox.configure(bg=LIGHT_MODE_BG) self.thru_checkbox.configure(fg=LIGHT_MODE_FG) self.thru_checkbox.configure(selectcolor=LIGHT_MODE_BG) diff --git a/classes/screen.py b/classes/screen.py index 19b114d..1951c09 100644 --- a/classes/screen.py +++ b/classes/screen.py @@ -1,21 +1,26 @@ # import statements -from classes.movement import Movement, SidebarGroup +from classes.movement import Linear, SidebarGroup, Angular from classes.converter import Converter as c from classes.constants import * import tkinter as tk from tkinter import ttk +import re import sys import os -from math import fmod +from math import fmod, atan2, pi # Window class encapsulates main logic class Window: def __init__(self, root, canvas): # initialize variables + self.mode = "movement" self.creating_movement = False self.start_point = (0, 0) self.end_point = (0, 0) + self.origin = (0, 0) + self.start_angle = 0 + self.extent = 0 self.editing_movement = 0 self.editing_index = -1 self.root = root @@ -25,6 +30,7 @@ def __init__(self, root, canvas): self.next_temp_line = None self.movements = [] self.sidebar_groups = [] + self.filename = tk.StringVar() # create scrollable canvas for sidebar self.sidebar_canvas = tk.Canvas(root, width=SIDEBAR_WIDTH, height=SCREEN_HEIGHT) @@ -49,8 +55,8 @@ def __init__(self, root, canvas): file = tk.Menu(self.root, tearoff=False) # add import, export, clear - file.add_command(label = "Import", command=self.import_script) - file.add_command(label = "Export", command=self.export_script) + file.add_command(label = "Import", command=self.choose_import_script) + file.add_command(label = "Export", command=self.export_script_name) file.add_command(label = "Clear", command= self.clear) # attach file submenu to main menu bar @@ -84,8 +90,10 @@ def __init__(self, root, canvas): # bind keyboard input to key_handler callback self.root.bind("", self.key_handler) - # bind mouse button input to click_handler callback - self.canvas.bind("