HUGE changes

- Added an about page that shows readme
- Added even more comments
- Added a playback feature
- 100x the pain and suffering!
This commit is contained in:
Daniel Carta
2024-07-26 13:59:15 -04:00
parent 6f9829f4f8
commit 99a298ec9b
5 changed files with 198 additions and 57 deletions
Vendored
BIN
View File
Binary file not shown.
+9 -10
View File
@@ -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?
+43
View File
@@ -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())
+95 -26
View File
@@ -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)
# Clear existing rectangles
while self.rectanglesLayout.count():
item = self.rectanglesLayout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
#Set the key frames width
window_width = self.rectanglesWidget.width()
if self.keyFrameData:
rect_width = window_width / len(self.keyFrameData)
# Calculate the width of each rectangle
total_width = self.width() - 80
rect_width = max(1, total_width // self.displayTicks)
rect_height = 100 # Adjustable key frame height
for index, frame in enumerate(self.keyFrameData):
rectWidget = QWidget()
rectWidget.setFixedSize(rect_width, rect_height)
# 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)
+44 -14
View File
@@ -5,21 +5,27 @@ 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)
@@ -29,39 +35,51 @@ class PathPlanner(QMainWindow):
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)
# 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 = []
# 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
@@ -69,15 +87,23 @@ class PathPlanner(QMainWindow):
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)
@@ -323,6 +349,10 @@ class PathPlanner(QMainWindow):
self.hide()
self.buttonEditor.show()
def showAbout(self):
self.about_window = AboutWindow()
self.about_window.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = PathPlanner()