diff --git a/.DS_Store b/.DS_Store index 5cda457..56632b4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 3366f78..98486e0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# autoPlanner +# Auto Planner (WIP) An auto creation tool for Ridgebotics 2025 -### Install +### Install: ```shell git clone https://https://github.com/Team4388/autoPlanner2025/tree/pyside @@ -10,10 +10,10 @@ cd autoPlanner2025 pip install -r requirements.txt python3 ./main.py ``` +# +## Usage: -### Usage: - -##### "Path Editor" Tab: +### "Path Editor" Tab: - Right click to add nodes - Left click on specific points to manipulate paths and nodes: @@ -24,19 +24,18 @@ python3 ./main.py - Double click on control points to make path, and robot movment continuous, while keeping the node clicked the at the same location (Smooth the path) - Press the 'r' key to delete the whole auto -##### "Button editor" Tab: +### "Button editor" Tab: - Click on specific frames on the timeline to change to that position + - Pressing or holding the left or right arrow keys will move the current frame - When selected on a frame, the robot's position in that time should show up. - Drag positional keyframes around to speed up and speed down the robot's travel between nodes -- While a frame is selected, Press the 'e' key to swap to button mode. -- In button mode select buttons on the driver and operator controllers. +- Pressing the 'e' key will pause and unpause the auto playback -##### "Export" Tab: +### "Export" Tab (not made yet): - Click export, and save to a file ### Known Bugs: -- Smoothing function is janky sometimes -- Sometimes the control points spawn in random locations but I cant seem to replicate it? \ No newline at end of file +- Smoothing function is janky sometimes \ No newline at end of file diff --git a/about.py b/about.py new file mode 100644 index 0000000..e24942e --- /dev/null +++ b/about.py @@ -0,0 +1,43 @@ +import sys +import os +from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QScrollArea +from PySide6.QtGui import QPixmap +from PySide6.QtCore import Qt + +class AboutWindow(QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("About") + self.setGeometry(100, 100, 700, 700) + + # Create a central widget and set the layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + layout.addWidget(scroll_area) + + # Create a widget to hold the content + content_widget = QWidget() + scroll_area.setWidget(content_widget) + content_layout = QVBoxLayout(content_widget) + + # Read the README file + readme_path = os.path.join(os.path.dirname(__file__), 'README.md') + try: + with open(readme_path, 'r') as file: + readme_content = file.read() + except FileNotFoundError: + readme_content = "README.md file not found." + + about_label = QLabel(readme_content) + about_label.setWordWrap(True) + about_label.setTextFormat(Qt.MarkdownText) + content_layout.addWidget(about_label) + +if __name__ == "__main__": + app = QApplication(sys.argv) + main_window = AboutWindow() + main_window.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/buttonEditor.py b/buttonEditor.py index cb53e6a..b9fb178 100644 --- a/buttonEditor.py +++ b/buttonEditor.py @@ -2,8 +2,8 @@ import sys import os import numpy as np from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget -from PySide6.QtGui import QPixmap, QPainter, QPen, QColor -from PySide6.QtCore import Qt, QPoint, QRect +from PySide6.QtGui import QPixmap, QPainter, QPen, QColor, QKeyEvent +from PySide6.QtCore import Qt, QPoint, QRect, QTimer ''' This window is the button editor @@ -13,7 +13,8 @@ The button editor takes the path that is made in the path planner and lets the u class ButtonEditor(QMainWindow): def __init__(self, pathPlanner): super().__init__() - # Initialization + + # Initialization for the window self.setWindowTitle("Button Editor") self.pathPlanner = pathPlanner @@ -68,7 +69,8 @@ class ButtonEditor(QMainWindow): self.matchTicks = self.matchLength * self.TPS # The amount of ticks in a match self.displayTickResolution = 6.25 # The number of ticks to divide the match ticks by self.displayTicks = round(self.matchTicks / self.displayTickResolution) # How many ticks to show the user - + self.paused = True # If the user has paused the auto + self.playbackSpeed = 0.25 # Speed multiplier for playback self.currentFrame = 1 # The frame that is currently selected self.keyFrameData = [{"isNode": False, "isButton": False} for _ in range(self.displayTicks)] # Dictionary for every tick including if that tick is a node frame or a button frame self.updateKeyFrameData() @@ -82,10 +84,38 @@ class ButtonEditor(QMainWindow): for idx in node_indices: self.keyFrameData[idx]["isNode"] = True + # Start the timer + self.timer = QTimer(self) + self.timer.timeout.connect(self.advanceFrame) + self.timer.start(int(1000 // (self.TPS * self.playbackSpeed))) + # Initialization self.setupFrames() + self.updateScene() self.updateRectangles() self.updateTimeLabel() + self.rectangleClicked(0) + + # Tell when the user presses a key down + def keyPressEvent(self, event: QKeyEvent): + old_frame = self.currentFrame + if event.key() == Qt.Key_Right and self.currentFrame < len(self.displayFrames): + self.currentFrame += 1 + elif event.key() == Qt.Key_Left and self.currentFrame > 1: + self.currentFrame -= 1 + elif event.key() == Qt.Key_E: + self.paused = not self.paused + if self.paused: + self.timer.stop() + else: + self.timer.start(int(1000 // (self.TPS * self.playbackSpeed))) + + print(self.paused) + + if old_frame != self.currentFrame: + self.updateRectangles() + self.updateScene() + self.updateTimeLabel() # Updating the key frames with data from the path planner def updateKeyFrameData(self): @@ -170,6 +200,25 @@ class ButtonEditor(QMainWindow): start_angle = self.pathPlanner.nodeAngles[i] if i < len(self.pathPlanner.nodeAngles) else 0 end_angle = self.pathPlanner.nodeAngles[i + 1] if i + 1 < len(self.pathPlanner.nodeAngles) else 0 angle = self.interpolateAngle(start_angle, end_angle, t) + + total_frames = self.displayTicks + frame_index = self.currentFrame - 1 + if 0 <= frame_index < total_frames: + overall_t = frame_index / (total_frames - 1) + num_segments = len(self.pathPlanner.coordinates) - 1 + segment_index = min(int(overall_t * num_segments), num_segments - 1) + segment_t = (overall_t * num_segments) - segment_index + + start = QPoint(self.pathPlanner.coordinates[segment_index][0], self.pathPlanner.coordinates[segment_index][1]) + end = QPoint(self.pathPlanner.coordinates[segment_index + 1][0], self.pathPlanner.coordinates[segment_index + 1][1]) + control1 = self.pathPlanner.controlPoints[segment_index][0] + control2 = self.pathPlanner.controlPoints[segment_index][1] + + point = self.pointOnBezierCurve(start, control1, control2, end, segment_t) + + start_angle = self.pathPlanner.nodeAngles[segment_index] if segment_index < len(self.pathPlanner.nodeAngles) else 0 + end_angle = self.pathPlanner.nodeAngles[segment_index + 1] if segment_index + 1 < len(self.pathPlanner.nodeAngles) else 0 + angle = self.interpolateAngle(start_angle, end_angle, segment_t) self.drawRobot(painter, point, angle) painter.end() # If you don't end the painter the app crashes @@ -210,33 +259,40 @@ class ButtonEditor(QMainWindow): # Update the frames on the bottom of the screen that the user interacts with def updateRectangles(self): - # Add the key frame widget - for i in reversed(range(self.rectanglesLayout.count())): - widgetToRemove = self.rectanglesLayout.itemAt(i).widget() - self.rectanglesLayout.removeWidget(widgetToRemove) - widgetToRemove.setParent(None) - - #Set the key frames width - window_width = self.rectanglesWidget.width() - if self.keyFrameData: - rect_width = window_width / len(self.keyFrameData) - - rect_height = 100 # Adjustable key frame height - for index, frame in enumerate(self.keyFrameData): - rectWidget = QWidget() - rectWidget.setFixedSize(rect_width, rect_height) + # Clear existing rectangles + while self.rectanglesLayout.count(): + item = self.rectanglesLayout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + # Calculate the width of each rectangle + total_width = self.width() - 80 + rect_width = max(1, total_width // self.displayTicks) + + # Create new rectangles + for i in range(self.displayTicks): + rect = QWidget() + rect.setFixedSize(rect_width, 100) - # Set the colors of the frames - if frame["isNode"]: - rectWidget.setStyleSheet("background-color: yellow;") + # Set the color based on frame type + if i == self.currentFrame - 1: + color = "red" + elif self.keyFrameData[i]["isNode"]: + color = "yellow" + elif self.keyFrameData[i]["isButton"]: + color = "blue" else: - if index % 2 == 0: - rectWidget.setStyleSheet("background-color: #ADD8E6;") - else: - rectWidget.setStyleSheet("background-color: #00008B;") + color = "#ADD8E6" if i % 2 == 0 else "#00008B" + rect.setStyleSheet(f"background-color: {color};") - rectWidget.mousePressEvent = lambda event, idx=index: self.rectangleClicked(idx) - self.rectanglesLayout.addWidget(rectWidget) + # Connect the click event + rect.mousePressEvent = lambda event, index=i: self.rectangleClicked(index) + self.rectanglesLayout.addWidget(rect) + + # Force layout update + self.rectanglesWidget.updateGeometry() + self.rectanglesLayout.update() # Set the current frame to the frame that the user clicked def rectangleClicked(self, index): @@ -257,6 +313,19 @@ class ButtonEditor(QMainWindow): milliseconds = int((current_time % 1) * 1000) self.timeLabel.setText(f"{minutes}:{seconds:02d}.{milliseconds:03d} / {self.matchLength:.3f} sec") + # The auto playback + def advanceFrame(self): + if not self.paused: + if self.currentFrame < len(self.displayFrames): + self.currentFrame += 1 + else: + self.currentFrame = 1 # Loop back to the start + + # Update + self.updateRectangles() + self.updateScene() + self.updateTimeLabel() + # App initialization if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/main.py b/main.py index 5df2c82..381ae93 100644 --- a/main.py +++ b/main.py @@ -5,79 +5,105 @@ from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QV from PySide6.QtGui import QPixmap, QMouseEvent, QPainter, QPen, QColor, QPainterPath, QPolygon, QFont, QKeyEvent from PySide6.QtCore import Qt, QPoint, QRect -from buttonEditor import ButtonEditor +from buttonEditor import ButtonEditor # Import the button editor +from about import AboutWindow + +''' +This is the path planner window +The path planner lets the user add nodes and control points to let them change the bezier curves and the path of the robot during the auto +''' class PathPlanner(QMainWindow): def __init__(self): super().__init__() + self.setWindowTitle("Path Planner") # Set the window title - self.setWindowTitle("Path Planner") - - self.coordinates = np.empty((0, 2), dtype=int) + # Set up the arrays + self.coordinates = np.empty((0, 2), dtype=int) # Make an empty array for the coordinates of objects self.controlPoints = [] self.rotationHandles = [] self.nodeAngles = [] + # Find the field png and then set the background to that png self.imageLabel = QLabel(self) - scriptDir = os.path.dirname(os.path.abspath(__file__)) imagePath = os.path.join(scriptDir, "images", "Field.png") self.pixmap = QPixmap(imagePath) - + if self.pixmap.isNull(): self.imageLabel.setText(f"Image not found at: {imagePath}") else: self.imageLabel.setPixmap(self.pixmap) + # Buttons at the top of the screen self.mainWindowButton = QPushButton("Main Window") self.mainWindowButton.clicked.connect(self.showMainWindow) self.buttonEditorButton = QPushButton("Button Editor") self.buttonEditorButton.clicked.connect(self.showButtonEditor) + self.aboutButton = QPushButton("About") + self.aboutButton.clicked.connect(self.showAbout) + # Button layouts buttonLayout = QHBoxLayout() buttonLayout.addWidget(self.mainWindowButton) buttonLayout.addWidget(self.buttonEditorButton) + buttonLayout.addWidget(self.aboutButton) + # Adding the button layout to the main layout layout = QVBoxLayout() layout.addLayout(buttonLayout) layout.addWidget(self.imageLabel) + # Set the button editor to that script self.buttonEditor = ButtonEditor(self) + # Defining the overall layout container = QWidget() container.setLayout(layout) self.setCentralWidget(container) + # Resize the window to all of the layouts and widgets added self.resize(self.pixmap.width(), self.pixmap.height() + 60) - self.setMouseTracking(True) - + # Let the app track the users mouse + self.setMouseTracking(True) self.lastClickPos = QPoint() - self.coordinates = np.empty((0, 2), dtype=int) - self.lastClickTime = 0 - self.nodeSize = 35 - self.handleSize = 15 - self.rotationHandleDistance = 35 - self.controlPoints = [] - self.rotationHandles = [] - self.nodeAngles = [] - self.draggingControlPoint = False + # Variables + self.coordinates = np.empty((0, 2), dtype=int) # Define the coordinate array again + self.lastClickTime = 0 # The last time the user clicked on the sceen + self.nodeSize = 35 # How large to make the nodes + self.handleSize = 15 # How large to make the rotation/control handles + self.rotationHandleDistance = 35 # How far the rotation handles are from the node + self.controlPoints = [] # Stores all of the control points + self.rotationHandles = [] # Stores all of the rotation handles + self.nodeAngles = [] # Stores all of the node angles + + # Tell what the user is currently dragging + self.draggingControlPoint = False self.draggingNode = False self.draggingRotationHandle = False self.draggingControlPointIndex = (-1, -1) self.draggingNodeIndex = -1 self.draggingRotationHandleIndex = -1 + # Tell when the user presses a key down def keyPressEvent(self, event: QKeyEvent): if event.key() == Qt.Key_R: + # Clear the auto self.showClearWarning() + + + # Tell when the user presses a mouse button def mousePressEvent(self, event: QMouseEvent): + # Get the position of the mouse click pos = self.imageLabel.mapFrom(self, event.position().toPoint()) x, y = pos.x(), pos.y() + # Tell if the mouse was click ed in the window if 0 <= x < self.pixmap.width() and 0 <= y < self.pixmap.height(): + # If right clicked, add a node if event.button() == Qt.RightButton: self.coordinates = np.vstack((self.coordinates, [x, y])) self.nodeAngles.append(0) @@ -321,7 +347,11 @@ class PathPlanner(QMainWindow): def showButtonEditor(self): self.hide() - self.buttonEditor.show() + self.buttonEditor.show() + + def showAbout(self): + self.about_window = AboutWindow() + self.about_window.show() if __name__ == "__main__": app = QApplication(sys.argv)